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.6.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)


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

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.



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

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

.code_schemeObject

The scheme #codes belong to.



171
172
173
# File 'lib/menuella/food_safety.rb', line 171

def code_scheme
  codes.scheme
end

.codesObject

The footnote-code scheme.



159
160
161
162
163
164
165
166
167
168
# File 'lib/menuella/food_safety.rb', line 159

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.



251
252
253
# File 'lib/menuella/food_safety.rb', line 251

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)


206
207
208
# File 'lib/menuella/food_safety.rb', line 206

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

.declaration_keysObject

Every declaration key, in canonical order.



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

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.



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
152
153
154
155
156
# File 'lib/menuella/food_safety.rb', line 126

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.



117
118
119
# File 'lib/menuella/food_safety.rb', line 117

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.



215
216
217
# File 'lib/menuella/food_safety.rb', line 215

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

.icon_namesObject

Every icon name that has a glyph, sorted.



190
191
192
# File 'lib/menuella/food_safety.rb', line 190

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"> 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247</pre> </td> <td> <pre class="code"><span class="info file"># File 'lib/menuella/food_safety.rb', line 226</span> <span class='rubyid_def def kw'>def</span> <span class='rubyid_icon_to_svg identifier id'>icon_to_svg</span><span class='lparen token'>(</span><span class='rubyid_name identifier id'>name</span><span class='comma token'>,</span> <span class='label val'>size:</span> <span class='integer val'>24</span><span class='comma token'>,</span> <span class='label val'>css_class:</span> <span class='rubyid_nil nil kw'>nil</span><span class='comma token'>,</span> <span class='label val'>title:</span> <span class='rubyid_nil nil kw'>nil</span><span class='rparen token'>)</span> <span class='rubyid_glyph identifier id'>glyph</span> <span class='assign token'>=</span> <span class='rubyid_icon identifier id'>icon</span><span class='lparen token'>(</span><span class='rubyid_name identifier id'>name</span><span class='rparen token'>)</span> <span class='rubyid_out identifier id'>out</span> <span class='assign token'>=</span> <span class='plus op'>+</span><span class='dstring node'>%(<svg xmlns="http://www.w3.org/2000/svg" viewBox="#{escape(glyph.view_box)}")</span> <span class='rubyid_out identifier id'>out</span> <span class='lshft op'><<</span> <span class='dstring node'>%( width="#{size.to_i}" height="#{size.to_i}" fill="none")</span> <span class='rubyid_out identifier id'>out</span> <span class='lshft op'><<</span> <span class='dstring node'>%( class="#{escape(css_class)}")</span> <span class='rubyid_if if_mod kw'>if</span> <span class='rubyid_css_class identifier id'>css_class</span> <span class='andop op'>&&</span> <span class='notop op'>!</span><span class='rubyid_css_class identifier id'>css_class</span><span class='dot token'>.</span><span class='rubyid_empty? fid id'>empty?</span> <span class='rubyid_out identifier id'>out</span> <span class='lshft op'><<</span> <span class='rubyid_if if kw'>if</span> <span class='rubyid_title identifier id'>title</span> <span class='andop op'>&&</span> <span class='notop op'>!</span><span class='rubyid_title identifier id'>title</span><span class='dot token'>.</span><span class='rubyid_empty? fid id'>empty?</span> <span class='dstring node'>%( role="img"><title>#{escape(title)}</title>)</span> <span class='rubyid_else else kw'>else</span> <span class='string val'>%( aria-hidden="true" focusable="false">)</span> <span class='rubyid_end end kw'>end</span> <span class='rubyid_glyph identifier id'>glyph</span><span class='dot token'>.</span><span class='rubyid_nodes identifier id'>nodes</span><span class='dot token'>.</span><span class='rubyid_each identifier id'>each</span> <span class='rubyid_do do kw'>do</span> <span class='bitor op'>|</span><span class='rubyid_node identifier id'>node</span><span class='bitor op'>|</span> <span class='rubyid_out identifier id'>out</span> <span class='lshft op'><<</span> <span class='dstring node'>"<#{node.tag}"</span> <span class='rubyid_node identifier id'>node</span><span class='dot token'>.</span><span class='rubyid_attributes identifier id'>attributes</span><span class='dot token'>.</span><span class='rubyid_each identifier id'>each</span> <span class='lbrace token'>{</span> <span class='bitor op'>|</span><span class='rubyid_key identifier id'>key</span><span class='comma token'>,</span> <span class='rubyid_value identifier id'>value</span><span class='bitor op'>|</span> <span class='rubyid_out identifier id'>out</span> <span class='lshft op'><<</span> <span class='dstring node'>%( #{key}="#{escape(value)}")</span> <span class='rbrace token'>}</span> <span class='rubyid_out identifier id'>out</span> <span class='lshft op'><<</span> <span class='string val'>"/>"</span> <span class='rubyid_end end kw'>end</span> <span class='rubyid_out identifier id'>out</span> <span class='lshft op'><<</span> <span class='string val'>"</svg>"</span> <span class='rubyid_out identifier id'>out</span><span class='dot token'>.</span><span class='rubyid_freeze identifier id'>freeze</span> <span class='rubyid_end end 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><script> document.getElementById("locale?-class_method").insertAdjacentHTML( "afterbegin", '<a class="permalink" href="/gems/menuella-food_safety/Menuella%2FFoodSafety%2Elocale%3F">permalink</a>' ); </script> <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"> 195 196 197</pre> </td> <td> <pre class="code"><span class="info file"># File 'lib/menuella/food_safety.rb', line 195</span> <span class='rubyid_def def kw'>def</span> <span class='rubyid_locale? fid id'>locale?</span><span class='lparen token'>(</span><span class='rubyid_value identifier id'>value</span><span class='rparen token'>)</span> <span class='rubyid_locales identifier id'>locales</span><span class='dot token'>.</span><span class='rubyid_include? fid id'>include?</span><span class='lparen token'>(</span><span class='rubyid_value identifier id'>value</span><span class='rparen token'>)</span> <span class='rubyid_end end 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><script> document.getElementById("locales-class_method").insertAdjacentHTML( "afterbegin", '<a class="permalink" href="/gems/menuella-food_safety/Menuella%2FFoodSafety%2Elocales">permalink</a>' ); </script> <div class="docstring"> <div class="discussion"> <p>Locales with a prebuilt bundle. Derived from the shipped <code>bundles/</code> directory, so adding a new locale is a matter of dropping <code>bundles/xx.json</code> in — no source list to keep in step.</p> </div> </div> <div class="tags"> </div><table class="source_code"> <tr> <td> <pre class="lines"> 109 110 111 112 113 114</pre> </td> <td> <pre class="code"><span class="info file"># File 'lib/menuella/food_safety.rb', line 109</span> <span class='rubyid_def def kw'>def</span> <span class='rubyid_locales identifier id'>locales</span> <span class='rubyid_@locales ivar id'>@locales</span> <span class='opasgn op'>||=</span> <span class='rubyid_Dir constant id'>Dir</span><span class='dot token'>.</span><span class='rubyid_glob identifier id'>glob</span><span class='lparen token'>(</span><span class='rubyid_File constant id'>File</span><span class='dot token'>.</span><span class='rubyid_join identifier id'>join</span><span class='lparen token'>(</span><span class='rubyid_DATA_DIR constant id'>DATA_DIR</span><span class='comma token'>,</span> <span class='string val'>"bundles"</span><span class='comma token'>,</span> <span class='string val'>"*.json"</span><span class='rparen token'>)</span><span class='rparen token'>)</span> <span class='dot token'>.</span><span class='rubyid_map identifier id'>map</span> <span class='lbrace token'>{</span> <span class='rubyid_File constant id'>File</span><span class='dot token'>.</span><span class='rubyid_basename identifier id'>basename</span><span class='lparen token'>(</span><span class='rubyid__1 identifier id'>_1</span><span class='comma token'>,</span> <span class='string val'>".json"</span><span class='rparen token'>)</span> <span class='rbrace token'>}</span> <span class='dot token'>.</span><span class='rubyid_sort identifier id'>sort</span> <span class='dot token'>.</span><span class='rubyid_freeze identifier id'>freeze</span> <span class='rubyid_end end kw'>end</span> </pre> </td> </tr> </table> </div> </div> </div> <div id="footer"> Generated on Sat Sep 19 17:37:44 2026 by <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a> 0.9.45 (ruby-4.0.7). </div> </div> </body> </html>