Module: Menuella::FoodSafety

Defined in:
lib/menuella/food_safety.rb,
lib/menuella/food_safety/version.rb

Overview

The Menuella food-safety vocabulary: EU Reg. 1169/2011 Annex II allergens and the Menuella declarations, in six languages.

Semantic keys instead of country-specific numbers — store the key, render the code, never the reverse.

de = Menuella::FoodSafety.disclosures("de")
de.allergens.find { _1.key == "WHEAT" }.declaration
# => "Enthält Getreide und glutenhaltige Erzeugnisse"

This module does no i18n. Hand it the locale your application already resolved; it will not sniff the environment, negotiate, or quietly fall back.

Everything it returns is frozen. The dataset is a shared constant, and a caller that could mutate one bundle would be mutating every other caller's copy in the same process.

Defined Under Namespace

Classes: Allergen, Codes, Declaration, Disclosures, Error, Icon, IconNode, UnknownIconError, UnsupportedLocaleError

Constant Summary collapse

VERSION =

Kept in step with every other binding by scripts/verify.mjs: one dataset, one version, released from one tag.

"1.3.0"

Class Method Summary collapse

Class Method Details

.allergen_key?(value) ⇒ Boolean

True when value is a current allergen key. Retired keys and display-only groups return false.

Returns:

  • (Boolean)


196
197
198
# File 'lib/menuella/food_safety.rb', line 196

def allergen_key?(value)
  allergen_keys.include?(value)
end

.allergen_keysObject

Every selectable allergen key, in canonical order.

Derived from the dataset, never hand-listed. Keys are locale-independent, so this reads the fallback bundle rather than taking a locale argument.



175
176
177
# File 'lib/menuella/food_safety.rb', line 175

def allergen_keys
  memo(:allergen_keys) { disclosures(fallback_locale).allergens.map(&:key).freeze }
end

.code_schemeObject

The scheme #codes belong to.



166
167
168
# File 'lib/menuella/food_safety.rb', line 166

def code_scheme
  codes.scheme
end

.codesObject

The footnote-code scheme.



154
155
156
157
158
159
160
161
162
163
# File 'lib/menuella/food_safety.rb', line 154

def codes
  memo(:codes) do
    raw = read("codes")
    Codes.new(
      scheme: raw.fetch("scheme").freeze,
      allergens: deep_freeze(raw.fetch("allergens")),
      declarations: deep_freeze(raw.fetch("declarations")),
    )
  end
end

.dataset(name) ⇒ Object

The raw packaged JSON for name, for tooling that wants the dataset rather than the objects. Returns a fresh string each call.



246
247
248
# File 'lib/menuella/food_safety.rb', line 246

def dataset(name)
  File.read(File.join(DATA_DIR, "#{name.sub(/\.json\z/, '')}.json"), encoding: "UTF-8")
end

.declaration_key?(value) ⇒ Boolean

True when value is a current declaration key.

Returns:

  • (Boolean)


201
202
203
# File 'lib/menuella/food_safety.rb', line 201

def declaration_key?(value)
  declaration_keys.include?(value)
end

.declaration_keysObject

Every declaration key, in canonical order.



180
181
182
# File 'lib/menuella/food_safety.rb', line 180

def declaration_keys
  memo(:declaration_keys) { disclosures(fallback_locale).declarations.map(&:key).freeze }
end

.disclosures(locale) ⇒ Object

Every disclosure for locale, ready to render.

Raises UnsupportedLocaleError rather than falling back: a silently wrong language on an allergen panel is worse than a loud failure. The caller knows which locales it supports, and #locale? is there to ask.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/menuella/food_safety.rb', line 121

def disclosures(locale)
  raise UnsupportedLocaleError, locale unless locale?(locale)

  memo(:bundles, locale) do
    raw = read("bundles/#{locale}")
    Disclosures.new(
      locale: raw.fetch("locale").freeze,
      fallback_locale: raw.fetch("fallbackLocale").freeze,
      allergens: raw.fetch("allergens").map { |a|
        Allergen.new(
          key: a.fetch("key").freeze,
          group: a.fetch("group").freeze,
          is_member: a.fetch("isMember"),
          icon: a.fetch("icon").freeze,
          name: a.fetch("name").freeze,
          declaration: a.fetch("declaration").freeze,
          description: a.fetch("description").freeze,
        )
      }.freeze,
      declarations: raw.fetch("declarations").map { |d|
        Declaration.new(
          key: d.fetch("key").freeze,
          category: d.fetch("category").freeze,
          icon: d.fetch("icon").freeze,
          name: d.fetch("name").freeze,
          description: d.fetch("description").freeze,
        )
      }.freeze,
    )
  end
end

.fallback_localeObject

The locale a bundle falls back to for anything it does not itself carry.



112
113
114
# File 'lib/menuella/food_safety.rb', line 112

def fallback_locale
  "en"
end

.icon(name) ⇒ Object

The glyph named name, as data.

Every shape paints with currentColor, so a glyph inherits the surrounding text colour and follows a light or dark theme with no second asset.



210
211
212
# File 'lib/menuella/food_safety.rb', line 210

def icon(name)
  icons.fetch(name) { raise UnknownIconError, name }
end

.icon_namesObject

Every icon name that has a glyph, sorted.



185
186
187
# File 'lib/menuella/food_safety.rb', line 185

def icon_names
  memo(:icon_names) { icons.keys.sort.freeze }
end

.icon_to_svg(name, size: 24, css_class: nil, title: nil) ⇒ Object

The glyph named name as an string, for anything that interpolates markup — server-rendered HTML, e-mail, PDF.

Decorative by default: emits aria-hidden unless title: is given, which switches it to role="img" with a </tt>. These glyphs carry legal meaning, so render one <em>alongside</em> its declaration text, never instead of it — the safe default is the free one.</p> </div> </div> <div class="tags"> </div><table class="source_code"> <tr> <td> <pre class="lines"> 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242</pre> </td> <td> <pre class="code"><span class="info file"># File 'lib/menuella/food_safety.rb', line 221</span> <span class='kw'>def</span> <span class='id identifier rubyid_icon_to_svg'>icon_to_svg</span><span class='lparen'>(</span><span class='id identifier rubyid_name'>name</span><span class='comma'>,</span> <span class='label'>size:</span> <span class='int'>24</span><span class='comma'>,</span> <span class='label'>css_class:</span> <span class='kw'>nil</span><span class='comma'>,</span> <span class='label'>title:</span> <span class='kw'>nil</span><span class='rparen'>)</span> <span class='id identifier rubyid_glyph'>glyph</span> <span class='op'>=</span> <span class='id identifier rubyid_icon'>icon</span><span class='lparen'>(</span><span class='id identifier rubyid_name'>name</span><span class='rparen'>)</span> <span class='id identifier rubyid_out'>out</span> <span class='op'>=</span> <span class='op'>+</span><span class='tstring'><span class='tstring_beg'>%(</span><span class='tstring_content'><svg xmlns="http://www.w3.org/2000/svg" viewBox="</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_escape'>escape</span><span class='lparen'>(</span><span class='id identifier rubyid_glyph'>glyph</span><span class='period'>.</span><span class='id identifier rubyid_view_box'>view_box</span><span class='rparen'>)</span><span class='embexpr_end'>}</span><span class='tstring_content'>"</span><span class='tstring_end'>)</span></span> <span class='id identifier rubyid_out'>out</span> <span class='op'><<</span> <span class='tstring'><span class='tstring_beg'>%(</span><span class='tstring_content'> width="</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_size'>size</span><span class='period'>.</span><span class='id identifier rubyid_to_i'>to_i</span><span class='embexpr_end'>}</span><span class='tstring_content'>" height="</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_size'>size</span><span class='period'>.</span><span class='id identifier rubyid_to_i'>to_i</span><span class='embexpr_end'>}</span><span class='tstring_content'>" fill="none"</span><span class='tstring_end'>)</span></span> <span class='id identifier rubyid_out'>out</span> <span class='op'><<</span> <span class='tstring'><span class='tstring_beg'>%(</span><span class='tstring_content'> class="</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_escape'>escape</span><span class='lparen'>(</span><span class='id identifier rubyid_css_class'>css_class</span><span class='rparen'>)</span><span class='embexpr_end'>}</span><span class='tstring_content'>"</span><span class='tstring_end'>)</span></span> <span class='kw'>if</span> <span class='id identifier rubyid_css_class'>css_class</span> <span class='op'>&&</span> <span class='op'>!</span><span class='id identifier rubyid_css_class'>css_class</span><span class='period'>.</span><span class='id identifier rubyid_empty?'>empty?</span> <span class='id identifier rubyid_out'>out</span> <span class='op'><<</span> <span class='kw'>if</span> <span class='id identifier rubyid_title'>title</span> <span class='op'>&&</span> <span class='op'>!</span><span class='id identifier rubyid_title'>title</span><span class='period'>.</span><span class='id identifier rubyid_empty?'>empty?</span> <span class='tstring'><span class='tstring_beg'>%(</span><span class='tstring_content'> role="img"><title></span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_escape'>escape</span><span class='lparen'>(</span><span class='id identifier rubyid_title'>title</span><span class='rparen'>)</span><span class='embexpr_end'>}</span><span class='tstring_content'></title></span><span class='tstring_end'>)</span></span> <span class='kw'>else</span> <span class='tstring'><span class='tstring_beg'>%(</span><span class='tstring_content'> aria-hidden="true" focusable="false"></span><span class='tstring_end'>)</span></span> <span class='kw'>end</span> <span class='id identifier rubyid_glyph'>glyph</span><span class='period'>.</span><span class='id identifier rubyid_nodes'>nodes</span><span class='period'>.</span><span class='id identifier rubyid_each'>each</span> <span class='kw'>do</span> <span class='op'>|</span><span class='id identifier rubyid_node'>node</span><span class='op'>|</span> <span class='id identifier rubyid_out'>out</span> <span class='op'><<</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'><</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_node'>node</span><span class='period'>.</span><span class='id identifier rubyid_tag'>tag</span><span class='embexpr_end'>}</span><span class='tstring_end'>"</span></span> <span class='id identifier rubyid_node'>node</span><span class='period'>.</span><span class='id identifier rubyid_attributes'>attributes</span><span class='period'>.</span><span class='id identifier rubyid_each'>each</span> <span class='lbrace'>{</span> <span class='op'>|</span><span class='id identifier rubyid_key'>key</span><span class='comma'>,</span> <span class='id identifier rubyid_value'>value</span><span class='op'>|</span> <span class='id identifier rubyid_out'>out</span> <span class='op'><<</span> <span class='tstring'><span class='tstring_beg'>%(</span><span class='tstring_content'> </span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_key'>key</span><span class='embexpr_end'>}</span><span class='tstring_content'>="</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_escape'>escape</span><span class='lparen'>(</span><span class='id identifier rubyid_value'>value</span><span class='rparen'>)</span><span class='embexpr_end'>}</span><span class='tstring_content'>"</span><span class='tstring_end'>)</span></span> <span class='rbrace'>}</span> <span class='id identifier rubyid_out'>out</span> <span class='op'><<</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>/></span><span class='tstring_end'>"</span></span> <span class='kw'>end</span> <span class='id identifier rubyid_out'>out</span> <span class='op'><<</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'></svg></span><span class='tstring_end'>"</span></span> <span class='id identifier rubyid_out'>out</span><span class='period'>.</span><span class='id identifier rubyid_freeze'>freeze</span> <span class='kw'>end</span></pre> </td> </tr> </table> </div> <div class="method_details "> <h3 class="signature " id="locale?-class_method"> .<strong>locale?</strong>(value) ⇒ <tt>Boolean</tt> </h3><div class="docstring"> <div class="discussion"> <p>True when <code>value</code> is a locale with a bundle.</p> </div> </div> <div class="tags"> <p class="tag_title">Returns:</p> <ul class="return"> <li> <span class='type'>(<tt>Boolean</tt>)</span> </li> </ul> </div><table class="source_code"> <tr> <td> <pre class="lines"> 190 191 192</pre> </td> <td> <pre class="code"><span class="info file"># File 'lib/menuella/food_safety.rb', line 190</span> <span class='kw'>def</span> <span class='id identifier rubyid_locale?'>locale?</span><span class='lparen'>(</span><span class='id identifier rubyid_value'>value</span><span class='rparen'>)</span> <span class='id identifier rubyid_locales'>locales</span><span class='period'>.</span><span class='id identifier rubyid_include?'>include?</span><span class='lparen'>(</span><span class='id identifier rubyid_value'>value</span><span class='rparen'>)</span> <span class='kw'>end</span></pre> </td> </tr> </table> </div> <div class="method_details "> <h3 class="signature " id="locales-class_method"> .<strong>locales</strong> ⇒ <tt>Object</tt> </h3><div class="docstring"> <div class="discussion"> <p>Locales with a prebuilt bundle.</p> </div> </div> <div class="tags"> </div><table class="source_code"> <tr> <td> <pre class="lines"> 107 108 109</pre> </td> <td> <pre class="code"><span class="info file"># File 'lib/menuella/food_safety.rb', line 107</span> <span class='kw'>def</span> <span class='id identifier rubyid_locales'>locales</span> <span class='ivar'>@locales</span> <span class='op'>||=</span> <span class='qwords_beg'>%w[</span><span class='tstring_content'>de</span><span class='words_sep'> </span><span class='tstring_content'>en</span><span class='words_sep'> </span><span class='tstring_content'>es</span><span class='words_sep'> </span><span class='tstring_content'>fr</span><span class='words_sep'> </span><span class='tstring_content'>it</span><span class='words_sep'> </span><span class='tstring_content'>tr</span><span class='tstring_end'>]</span></span><span class='period'>.</span><span class='id identifier rubyid_freeze'>freeze</span> <span class='kw'>end</span></pre> </td> </tr> </table> </div> </div> </div> <div id="footer"> Generated on Sun Aug 2 18:32:12 2026 by <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a> 0.9.45 (ruby-4.0.2). </div> </div> </body> </html>