haveapi 0.32.1 → 0.32.2

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -8
  3. data/README.md +3 -422
  4. data/Rakefile +17 -7
  5. data/benchmarks/performance_compare.rb +27 -6
  6. data/doc/cs/client-examples.md +535 -0
  7. data/doc/cs/create-client.md +118 -0
  8. data/doc/cs/json-schema.html +1182 -0
  9. data/doc/cs/protocol.md +595 -0
  10. data/doc/cs/readme.md +432 -0
  11. data/doc/cs/typed-input-validation.md +122 -0
  12. data/doc/{client-examples.md → en/client-examples.md} +20 -10
  13. data/doc/{documentation-ui.md → en/documentation-ui.md} +42 -6
  14. data/doc/{performance.md → en/performance.md} +11 -0
  15. data/doc/{protocol.md → en/protocol.md} +2 -2
  16. data/doc/en/readme.md +423 -0
  17. data/haveapi.gemspec +2 -2
  18. data/lib/haveapi/client_example.rb +18 -5
  19. data/lib/haveapi/client_examples/curl.rb +5 -5
  20. data/lib/haveapi/client_examples/dynamic_helpers.rb +10 -10
  21. data/lib/haveapi/client_examples/go_client.rb +1 -1
  22. data/lib/haveapi/client_examples/http.rb +4 -4
  23. data/lib/haveapi/client_examples/js_client.rb +10 -10
  24. data/lib/haveapi/client_examples/php_client.rb +24 -25
  25. data/lib/haveapi/client_examples/python_client.rb +11 -11
  26. data/lib/haveapi/client_examples/ruby_cli.rb +6 -9
  27. data/lib/haveapi/client_examples/ruby_client.rb +24 -16
  28. data/lib/haveapi/client_examples/rust_client.rb +1 -1
  29. data/lib/haveapi/client_examples/ts_generated_client.rb +1 -1
  30. data/lib/haveapi/documentation_context.rb +14 -2
  31. data/lib/haveapi/documentation_helpers.rb +92 -15
  32. data/lib/haveapi/documentation_locale.rb +65 -0
  33. data/lib/haveapi/documentation_oauth.rb +12 -12
  34. data/lib/haveapi/documentation_search.rb +5 -5
  35. data/lib/haveapi/documentation_validators.rb +43 -33
  36. data/lib/haveapi/locales/cs.yml +468 -0
  37. data/lib/haveapi/locales/en.yml +467 -0
  38. data/lib/haveapi/public/css/documentation.css +8 -4
  39. data/lib/haveapi/public/js/documentation.js +73 -20
  40. data/lib/haveapi/server.rb +32 -47
  41. data/lib/haveapi/shared_documentation_helpers.rb +53 -5
  42. data/lib/haveapi/version.rb +1 -1
  43. data/lib/haveapi/views/doc_layout.erb +3 -2
  44. data/lib/haveapi/views/doc_sidebars/create-client.erb +10 -10
  45. data/lib/haveapi/views/doc_sidebars/json-schema.erb +2 -2
  46. data/lib/haveapi/views/doc_sidebars/protocol.erb +24 -24
  47. data/lib/haveapi/views/documentation/search_form.erb +5 -4
  48. data/lib/haveapi/views/documentation/search_results.erb +6 -6
  49. data/lib/haveapi/views/index.erb +3 -3
  50. data/lib/haveapi/views/main_layout.erb +4 -4
  51. data/lib/haveapi/views/shared_documentation.erb +8 -5
  52. data/lib/haveapi/views/shared_sidebar.erb +11 -11
  53. data/lib/haveapi/views/version_page/auth_body.erb +7 -7
  54. data/lib/haveapi/views/version_page/client_auth.erb +1 -1
  55. data/lib/haveapi/views/version_page/client_example.erb +1 -1
  56. data/lib/haveapi/views/version_page/client_init.erb +1 -1
  57. data/lib/haveapi/views/version_page/client_overview.erb +7 -5
  58. data/lib/haveapi/views/version_page/example_panels.erb +5 -2
  59. data/lib/haveapi/views/version_page/resource_body.erb +24 -24
  60. data/lib/haveapi/views/version_page/resource_directory.erb +2 -2
  61. data/lib/haveapi/views/version_page/usage.erb +5 -5
  62. data/lib/haveapi/views/version_page.erb +11 -11
  63. data/lib/haveapi/views/version_sidebar/auth_nav.erb +2 -2
  64. data/lib/haveapi/views/version_sidebar/authentication_control.erb +8 -8
  65. data/lib/haveapi/views/version_sidebar/authentication_status.erb +1 -1
  66. data/lib/haveapi/views/version_sidebar/brand.erb +16 -5
  67. data/lib/haveapi/views/version_sidebar.erb +33 -18
  68. data/spec/client_examples/assets/documentation_test.cjs +107 -9
  69. data/spec/client_examples/comment_capture.rb +72 -0
  70. data/spec/client_examples/comments_spec.rb +162 -0
  71. data/spec/client_examples/context_spec.rb +18 -2
  72. data/spec/client_examples/detail_spec.rb +68 -0
  73. data/spec/documentation/client_overview_spec.rb +60 -0
  74. data/spec/documentation/example_selection_spec.rb +192 -0
  75. data/spec/documentation/language_settings_spec.rb +132 -0
  76. data/spec/documentation/locale_spec.rb +367 -0
  77. data/spec/documentation/navigation_spec.rb +61 -3
  78. data/spec/documentation/oauth_spec.rb +42 -0
  79. data/spec/documentation/search_index_routes_spec.rb +8 -4
  80. data/spec/documentation/search_spec.rb +1 -1
  81. data/spec/documentation/shared_pages_spec.rb +63 -2
  82. data/spec/documentation/validators_spec.rb +25 -0
  83. data/spec/extensions/exception_mailer_spec.rb +2 -2
  84. data/spec/packaging_spec.rb +68 -9
  85. data/spec/performance/event_baseline_spec.rb +1 -1
  86. data/spec/performance/source_versions_spec.rb +111 -2
  87. metadata +24 -11
  88. /data/doc/{create-client.md → en/create-client.md} +0 -0
  89. /data/doc/{documentation-oauth.md → en/documentation-oauth.md} +0 -0
  90. /data/doc/{json-schema.html → en/json-schema.html} +0 -0
  91. /data/doc/{typed-input-validation.md → en/typed-input-validation.md} +0 -0
@@ -3,6 +3,29 @@
3
3
 
4
4
  var storageKey = 'haveapi.docs.preferences.v1';
5
5
 
6
+ var messages = Object.create(null);
7
+
8
+ function setDocumentationMessages(value) {
9
+ messages = value && typeof value === 'object' && !Array.isArray(value) ? value : Object.create(null);
10
+ }
11
+
12
+ function documentationMessage(key, values) {
13
+ var text = typeof messages[key] === 'string' ? messages[key] : key;
14
+ return text.replace(/%\{([a-z_]+)\}/g, function (placeholder, name) {
15
+ return values && Object.prototype.hasOwnProperty.call(values, name) ? String(values[name]) : placeholder;
16
+ });
17
+ }
18
+
19
+ function documentationLanguageURL(href, locale) {
20
+ if (['en', 'cs'].indexOf(locale) < 0) return null;
21
+ try {
22
+ var destination = new URL(href);
23
+ if (!/^https?:$/.test(destination.protocol)) return null;
24
+ destination.searchParams.set('doc_lang', locale);
25
+ return destination.href;
26
+ } catch (error) { return null; }
27
+ }
28
+
6
29
  function preferences(value) {
7
30
  var source = value && typeof value === 'object' ? value : {};
8
31
  var clients = Object.create(null);
@@ -131,8 +154,8 @@
131
154
  var button = doc.createElement('button');
132
155
  button.type = 'button';
133
156
  button.setAttribute('data-copy-example', '');
134
- button.setAttribute('aria-label', 'Copy example');
135
- button.title = 'Copy example';
157
+ button.setAttribute('aria-label', documentationMessage('copy_example'));
158
+ button.title = documentationMessage('copy_example');
136
159
  var icon = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
137
160
  icon.setAttribute('viewBox', '0 0 24 24');
138
161
  icon.setAttribute('aria-hidden', 'true');
@@ -160,7 +183,7 @@
160
183
  var current = generation;
161
184
  copyText(code.textContent, win, doc, navigator).then(function (success) {
162
185
  if (current !== generation || visibleExampleCode(panel) !== code) return;
163
- status.textContent = success ? 'Copied' : 'Select the code and copy it.';
186
+ status.textContent = success ? documentationMessage('copied') : documentationMessage('copy_failed');
164
187
  });
165
188
  });
166
189
  });
@@ -324,12 +347,13 @@
324
347
  if (!/^https?:$/.test(base.protocol) || base.origin !== current.origin || base.username || base.password
325
348
  || base.search || base.hash || !base.pathname.endsWith('/') || url.origin !== base.origin
326
349
  || url.pathname !== base.pathname || url.username || url.password || url.hash
327
- || Array.from(url.searchParams).length !== 2 || url.searchParams.getAll('page').length !== 1
350
+ || Array.from(url.searchParams).length !== (url.searchParams.has('doc_lang') ? 3 : 2)
351
+ || (url.searchParams.has('doc_lang') && (url.searchParams.getAll('doc_lang').length !== 1 || ['en', 'cs'].indexOf(url.searchParams.get('doc_lang')) < 0)) || url.searchParams.getAll('page').length !== 1
328
352
  || url.searchParams.get('page') !== 'resources' || url.searchParams.getAll('search_index').length !== 1
329
353
  || typeof version !== 'string' || !version || (key !== '' && !/^[a-f0-9]{64}$/.test(key))) return null;
330
354
  if (url.searchParams.get('search_index') !== (key || 'current')) return null;
331
355
  if (bootstrap) { url.searchParams.set('search_index', 'current'); key = ''; }
332
- return { url: url.href, origin: base.origin, base_path: base.pathname, version: version, key: key };
356
+ return { url: url.href, origin: base.origin, base_path: base.pathname, version: version, key: key, locale: url.searchParams.get('doc_lang') };
333
357
  } catch (error) { return null; }
334
358
  }
335
359
 
@@ -355,6 +379,7 @@
355
379
  var index = envelope.index;
356
380
  shape(index, ['format', 'version', 'base_path', 'locale', 'visibility', 'resources', 'actions', 'parameters']);
357
381
  if (index.format !== 1 || index.version !== expected.version || index.base_path !== expected.base_path
382
+ || (expected.locale && index.locale !== expected.locale)
358
383
  || ['anonymous', 'authenticated'].indexOf(index.visibility) < 0) fail();
359
384
  ['version', 'base_path', 'locale', 'visibility'].forEach(function (field) { string(index[field]); });
360
385
  if (!Array.isArray(index.resources) || !Array.isArray(index.actions) || !Array.isArray(index.parameters)) fail();
@@ -462,7 +487,7 @@
462
487
  var link = doc.createElement('a');
463
488
  var name = result.kind === 'resource' ? result.resource_path.join('.') : result.name;
464
489
  link.href = searchResultURL(index, result, origin);
465
- link.textContent = result.kind.charAt(0).toUpperCase() + result.kind.slice(1) + ': ';
490
+ link.textContent = documentationMessage(result.kind) + ': ';
466
491
  var code = doc.createElement('code');
467
492
  code.textContent = name;
468
493
  link.appendChild(code);
@@ -473,8 +498,8 @@
473
498
  } else {
474
499
  context.push(result.resource_path.join('.') + (result.action_name !== null ? ' · ' + result.action_name : ''));
475
500
  }
476
- if (result.kind === 'parameter') context.push(result.direction.charAt(0).toUpperCase() + result.direction.slice(1) + ' · ' + result.type);
477
- if (result.authentication !== null) context.push('Authentication: ' + result.authentication);
501
+ if (result.kind === 'parameter') context.push(documentationMessage(result.direction) + ' · ' + result.type);
502
+ if (result.authentication !== null) context.push(documentationMessage('authentication', { provider: result.authentication }));
478
503
  if (result.kind !== 'resource' && typeof result.label === 'string' && result.label !== result.name) context.push(result.label);
479
504
  context.forEach(function (text) {
480
505
  var detail = doc.createElement('span');
@@ -523,7 +548,7 @@
523
548
 
524
549
  // Pure helpers keep saved-state and legacy-link contracts independently testable.
525
550
  if (typeof module !== 'undefined' && module.exports) {
526
- module.exports = { preferences: preferences, readPreferences: readPreferences, clientChoice: clientChoice,
551
+ module.exports = { setDocumentationMessages: setDocumentationMessages, documentationLanguageURL: documentationLanguageURL, documentationMessage: documentationMessage, preferences: preferences, readPreferences: readPreferences, clientChoice: clientChoice,
527
552
  detailLevels: detailLevels, effectiveDetail: effectiveDetail, exampleDetail: exampleDetail, copyText: copyText,
528
553
  exampleCopyControls: exampleCopyControls, disclosureMenus: disclosureMenus,
529
554
  legacyTarget: legacyTarget, loginTarget: loginTarget,
@@ -561,12 +586,27 @@
561
586
  try { return element ? JSON.parse(element.textContent) : fallback; }
562
587
  catch (error) { return fallback; }
563
588
  };
589
+ setDocumentationMessages(json('[data-documentation-messages]', {}));
564
590
  var metadata = json('[data-documentation-clients]', []);
565
591
  var clients = Array.isArray(metadata) ? metadata.filter(function (client, index, list) {
566
592
  return client && typeof client.id === 'string' && typeof client.language === 'string'
567
593
  && typeof client.language_label === 'string' && client.id && client.language
568
594
  && list.findIndex(function (entry) { return entry && entry.id === client.id; }) === index;
569
595
  }) : [];
596
+ all('[data-example-preferences]').forEach(function (form) {
597
+ try {
598
+ var selected = JSON.parse(form.getAttribute('data-example-selection'));
599
+ var client = selected && clients.find(function (entry) { return entry.id === selected.client && entry.language === selected.language; });
600
+ if (client) {
601
+ state.language = client.language;
602
+ state.clients[client.language] = client.id;
603
+ state.detail = selected.detail === 'thorough' ? 'thorough' : 'simple';
604
+ }
605
+ } catch (error) { /* Invalid optional selection leaves saved preferences available. */ }
606
+ form.addEventListener('submit', function (event) { event.preventDefault(); });
607
+ var apply = form.querySelector('[data-apply-examples]');
608
+ if (apply) apply.hidden = true;
609
+ });
570
610
  var sidebar = root.querySelector('[data-docs-sidebar]');
571
611
  var examples = all('[data-example]');
572
612
  var sections = all('[data-doc-section]');
@@ -685,8 +725,8 @@
685
725
  all('[data-detail-select]').forEach(function (select) {
686
726
  var levels = choice ? detailLevels(choice) : [];
687
727
  select.replaceChildren();
688
- levels.forEach(function (detail) { option(select, detail, detail === 'thorough' ? 'With error handling' : 'Simple'); });
689
- (select.closest('[data-detail-control]') || select).hidden = false;
728
+ levels.forEach(function (detail) { option(select, detail, documentationMessage(detail === 'thorough' ? 'thorough' : 'simple')); });
729
+ (select.closest('[data-detail-control]') || select).hidden = levels.length < 2;
690
730
  select.disabled = levels.length < 2;
691
731
  select.value = effectiveDetail(state, choice);
692
732
  });
@@ -696,8 +736,8 @@
696
736
  if (status) {
697
737
  var fallback = choice && ((state.language && state.language !== choice.language)
698
738
  || (state.clients[choice.language] && state.clients[choice.language] !== choice.id));
699
- status.textContent = fallback ? 'Your saved example is unavailable. Showing ' + choice.language_label
700
- + (choice.variant ? ' (' + choice.variant + ')' : '') + '.' : '';
739
+ status.textContent = fallback ? documentationMessage('saved_example_unavailable', { client: choice.language_label
740
+ + (choice.variant ? ' (' + choice.variant + ')' : '') }) : '';
701
741
  status.hidden = !fallback;
702
742
  }
703
743
  }
@@ -724,7 +764,7 @@
724
764
  unavailable = document.createElement('p');
725
765
  unavailable.setAttribute('data-example-unavailable', '');
726
766
  unavailable.className = 'docs-notice';
727
- unavailable.textContent = 'No example is available for this client.';
767
+ unavailable.textContent = documentationMessage('example_unavailable');
728
768
  example.appendChild(unavailable);
729
769
  }
730
770
  unavailable.hidden = !!chosen;
@@ -762,6 +802,20 @@
762
802
  });
763
803
  }
764
804
 
805
+ all('[data-documentation-language]').forEach(function (form) {
806
+ var select = form.querySelector('[name="doc_lang"]');
807
+ if (!select) return;
808
+ function navigate(event) {
809
+ var destination = documentationLanguageURL(window.location.href, select.value);
810
+ if (!destination) return;
811
+ if (event) event.preventDefault();
812
+ window.location.assign(destination);
813
+ }
814
+ form.addEventListener('submit', navigate);
815
+ select.addEventListener('change', navigate);
816
+ var submit = form.querySelector('[data-documentation-language-submit]');
817
+ if (submit) submit.hidden = true;
818
+ });
765
819
  all('[data-language-select]').forEach(function (select) {
766
820
  select.addEventListener('change', function () { state.language = select.value; persist(); updatePreservingPosition(select); });
767
821
  });
@@ -841,9 +895,8 @@
841
895
  list.appendChild(renderSearchResult(document, snapshot.index, result, window.location.origin));
842
896
  });
843
897
  preview.replaceChildren(list);
844
- searchStatus.textContent = !results.results.length ? 'No matches. Try another resource, action or parameter name.'
845
- : results.more ? 'Showing ' + results.results.length + ' results. More matches are available; refine your search.'
846
- : results.results.length + (results.results.length === 1 ? ' result.' : ' results.');
898
+ searchStatus.textContent = documentationMessage(!results.results.length ? 'search_empty' : results.more ? 'search_more'
899
+ : results.results.length === 1 ? 'search_one' : 'search_count', { count: results.results.length });
847
900
  suggestions.hidden = false;
848
901
  }
849
902
  function intent() {
@@ -853,18 +906,18 @@
853
906
  var config = searchConfiguration(form.action, form.getAttribute('data-search-index-url'),
854
907
  form.getAttribute('data-search-view-key'), form.getAttribute('data-search-version'), window.location.href, bootstrapSearch);
855
908
  if (!config) {
856
- if (query) { suggestions.hidden = false; searchStatus.textContent = 'Search could not be loaded. Submit to open the results page.'; }
909
+ if (query) { suggestions.hidden = false; searchStatus.textContent = documentationMessage('search_failed'); }
857
910
  return;
858
911
  }
859
912
  var loaded = indexLoader.peek(config);
860
913
  if (loaded) { if (query && Array.from(query).length <= 256) render(loaded, query); return; }
861
- if (query) { suggestions.hidden = false; searchStatus.textContent = 'Loading search…'; }
914
+ if (query) { suggestions.hidden = false; searchStatus.textContent = documentationMessage('search_loading'); }
862
915
  indexLoader.load(config).then(function (snapshot) {
863
916
  if (current !== generation || input.value.trim() !== query || !query || Array.from(query).length > 256) return;
864
917
  render(snapshot, query);
865
918
  }).catch(function () {
866
919
  if (current === generation && query) {
867
- searchStatus.textContent = 'Search could not be loaded. Submit to open the results page.';
920
+ searchStatus.textContent = documentationMessage('search_failed');
868
921
  suggestions.hidden = false;
869
922
  }
870
923
  });
@@ -5,6 +5,7 @@ require 'cgi'
5
5
  require 'haveapi/hooks'
6
6
  require 'haveapi/documentation_clients'
7
7
  require 'haveapi/documentation_helpers'
8
+ require 'haveapi/documentation_locale'
8
9
  require 'haveapi/documentation_validators'
9
10
  require 'haveapi/documentation_search'
10
11
  require 'haveapi/shared_documentation_helpers'
@@ -237,7 +238,9 @@ module HaveAPI
237
238
  end
238
239
 
239
240
  def doc(file)
240
- markdown :"../../../doc/#{file}", with_toc_data: true, tables: true, fenced_code_blocks: true, lax_spacing: true
241
+ path = documentation_article_path(file)
242
+ shared_documentation_links(markdown(File.read(path), with_toc_data: true, tables: true,
243
+ fenced_code_blocks: true, lax_spacing: true))
241
244
  end
242
245
 
243
246
  def base_url
@@ -255,7 +258,7 @@ module HaveAPI
255
258
  @documentation_context ||= HaveAPI::ClientExample::Context.new(
256
259
  version: @v, host:, base_url: "#{base_url}#{root.end_with?('/') ? root : "#{root}/"}",
257
260
  version_url: "#{base_url}#{root.end_with?('/') ? root : "#{root}/"}v#{@v}/",
258
- description: @help
261
+ description: @help, locale: documentation_locale
259
262
  )
260
263
  profile = if client
261
264
  settings.api_server.documentation_client_profiles(@v)[client.code&.to_sym]
@@ -274,7 +277,8 @@ module HaveAPI
274
277
  end
275
278
 
276
279
  def documentation_clients
277
- HaveAPI::ClientExample.clients.select { |client| client.available?(documentation_context(client)) }
280
+ # The server's @documentation_clients owns profiles, not this request list.
281
+ @documentation_available_clients ||= HaveAPI::ClientExample.clients.select { |client| client.available?(documentation_context(client)) } # rubocop:disable Naming/MemoizedInstanceVariableName
278
282
  end
279
283
 
280
284
  def documentation_panels(kind, auth_method: nil, auth_description: nil, **changes)
@@ -513,6 +517,7 @@ module HaveAPI
513
517
  helpers ServerHelpers
514
518
  helpers DocHelpers
515
519
  helpers DocumentationHelpers
520
+ helpers DocumentationLocale
516
521
  helpers DocumentationSearch::Helpers
517
522
  helpers SharedDocumentationHelpers
518
523
  helpers DocumentationOAuth::Helpers
@@ -561,23 +566,25 @@ module HaveAPI
561
566
  [version, documentation_authenticated?(version)]
562
567
  end
563
568
 
564
- @api = settings.api_server.describe(Context.new(
565
- settings.api_server,
566
- user: auth_users_by_version[settings.api_server.default_version],
567
- params:,
568
- auth_users_by_version:
569
- ))
570
-
571
- if single_version
572
- version = settings.api_server.default_version
573
- @documentation_index_account = {
574
- version:, user: auth_users_by_version[version],
575
- authentication: @api[:versions].fetch(version).fetch(:authentication)
576
- }
577
- end
569
+ with_documentation_locale do
570
+ @api = settings.api_server.describe(Context.new(
571
+ settings.api_server,
572
+ user: auth_users_by_version[settings.api_server.default_version],
573
+ params:,
574
+ auth_users_by_version:
575
+ ))
576
+
577
+ if single_version
578
+ version = settings.api_server.default_version
579
+ @documentation_index_account = {
580
+ version:, user: auth_users_by_version[version],
581
+ authentication: @api[:versions].fetch(version).fetch(:authentication)
582
+ }
583
+ end
578
584
 
579
- content_type 'text/html'
580
- render_documentation_index
585
+ content_type 'text/html'
586
+ render_documentation_index
587
+ end
581
588
  end
582
589
 
583
590
  @sinatra.options @root do
@@ -621,36 +628,14 @@ module HaveAPI
621
628
 
622
629
  # Shared documentation keeps the existing content and URL aliases.
623
630
  @sinatra.get "#{@root}doc" do
624
- render_shared_readme
625
- end
626
-
627
- @sinatra.get "#{@root}doc/readme" do
628
- render_shared_readme
629
- end
630
-
631
- @sinatra.get "#{@root}doc/json-schema" do
632
- begin
633
- content = File.read(File.join(settings.root, '../../doc/json-schema.html'))
634
- rescue Errno::ENOENT
635
- halt 404
636
- end
637
- render_shared_documentation(content:, title: 'JSON schema', sidebar: erb(:'doc_sidebars/json-schema'))
631
+ with_documentation_locale { render_shared_article('readme') }
638
632
  end
639
633
 
640
- @sinatra.get %r{#{@root}doc/([^.]+)(\.md)?} do |file, _|
641
- next render_shared_readme if file == 'index'
634
+ @sinatra.get %r{#{@root}doc/([a-z0-9-]+)(\.md)?} do |file, extension|
635
+ halt 404 if extension && %w[readme json-schema].include?(file)
642
636
 
643
- begin
644
- content = doc(file)
645
- rescue Errno::ENOENT
646
- halt 404
647
- end
648
- sidebar = begin
649
- erb :"doc_sidebars/#{file}"
650
- rescue Errno::ENOENT
651
- nil
652
- end
653
- render_shared_documentation(content:, title: file.humanize, sidebar:)
637
+ file = 'readme' if file == 'index'
638
+ with_documentation_locale { render_shared_article(file) }
654
639
  end
655
640
 
656
641
  # Retain old documentation links without engaging browser credentials.
@@ -693,7 +678,7 @@ module HaveAPI
693
678
 
694
679
  headers 'Cache-Control' => 'private, no-store'
695
680
  query = request.query_string
696
- halt 400, 'Invalid documentation query' if query.bytes.any? { |byte| byte < 32 || byte == 127 }
681
+ halt 400, documentation_t('errors.query') if query.bytes.any? { |byte| byte < 32 || byte == 127 }
697
682
  destination = url(prefix, false)
698
683
  destination += "?#{query}" unless query.empty?
699
684
  halt 308, { 'Location' => destination }, ''
@@ -1,7 +1,7 @@
1
1
  module HaveAPI
2
2
  module SharedDocumentationHelpers
3
3
  def render_documentation_index
4
- render_neutral_documentation(content: erb(:index), title: 'API versions', page: 'versions')
4
+ render_neutral_documentation(content: erb(:index), title: documentation_t('labels.api_versions'), page: 'versions')
5
5
  end
6
6
 
7
7
  def render_shared_documentation(content:, title:, sidebar: nil)
@@ -9,9 +9,57 @@ module HaveAPI
9
9
  end
10
10
 
11
11
  def render_shared_readme
12
- content = markdown(File.read("#{settings.views}/../../../README.md"),
13
- with_toc_data: true, tables: true, fenced_code_blocks: true, lax_spacing: true)
14
- render_shared_documentation(content:, title: 'HaveAPI internals')
12
+ render_shared_article('readme')
13
+ end
14
+
15
+ def documentation_article_path(article, extension: 'md')
16
+ halt 404 unless article.match?(/\A[a-z0-9-]+\z/) && %w[md html].include?(extension)
17
+
18
+ directory = File.expand_path('../../doc', __dir__)
19
+ locale = documentation_locale
20
+ path = File.join(directory, locale, "#{article}.#{extension}")
21
+ unless File.file?(path)
22
+ locale = 'en'
23
+ path = File.join(directory, locale, "#{article}.#{extension}")
24
+ end
25
+ halt 404 unless File.file?(path)
26
+
27
+ @documentation_article_locale = locale
28
+ path
29
+ end
30
+
31
+ def render_shared_article(article)
32
+ @documentation_article_id = article
33
+ content = if article == 'json-schema'
34
+ File.read(documentation_article_path(article, extension: 'html'))
35
+ else
36
+ doc(article)
37
+ end
38
+ title = documentation_t("articles.#{article.tr('-', '_')}", default: article.humanize)
39
+ sidebar = begin
40
+ erb :"doc_sidebars/#{article}"
41
+ rescue Errno::ENOENT
42
+ nil
43
+ end
44
+ render_shared_documentation(content:, title:, sidebar:)
45
+ rescue Errno::ENOENT
46
+ halt 404
47
+ end
48
+
49
+ def shared_documentation_links(content)
50
+ content.gsub(/(href|src)="([^"#]*)(#[^"]*)?"/) do |attribute|
51
+ kind = Regexp.last_match(1)
52
+ path = Regexp.last_match(2)
53
+ fragment = Regexp.last_match(3).to_s
54
+ if path.match?(%r{\A(?:(?:\.\./)*(?:en/|cs/)?|/doc/(?:en/|cs/)?)[a-z0-9-]+\.md\z})
55
+ article = File.basename(path, '.md')
56
+ %(#{kind}="#{escape_html(shared_documentation_url("doc/#{article}"))}#{fragment}")
57
+ elsif path.match?(%r{\A(?:\.\./)+[a-z0-9-]+\.png\z})
58
+ %(#{kind}="#{escape_html(shared_documentation_url("doc/#{File.basename(path)}"))}#{fragment}")
59
+ else
60
+ attribute
61
+ end
62
+ end
15
63
  end
16
64
 
17
65
  def shared_documentation_url(path = '')
@@ -47,7 +95,7 @@ module HaveAPI
47
95
  def shared_documentation_tables(content)
48
96
  # Trusted repository HTML is rendered before this presentation-only pass.
49
97
  content.gsub(%r{<table\b[^>]*>.*?</table>}m) do |table|
50
- %(<div class="docs-table-scroll" role="region" aria-label="Documentation table" tabindex="0">#{table}</div>)
98
+ %(<div class="docs-table-scroll" role="region" aria-label="#{escape_html(documentation_t('accessibility.documentation_table'))}" tabindex="0">#{table}</div>)
51
99
  end
52
100
  end
53
101
  end
@@ -1,4 +1,4 @@
1
1
  module HaveAPI
2
2
  PROTOCOL_VERSION = '2.0'.freeze
3
- VERSION = '0.32.1'.freeze
3
+ VERSION = '0.32.2'.freeze
4
4
  end
@@ -1,14 +1,15 @@
1
1
  <% yield %>
2
2
  <% if @documentation_ui %>
3
3
  <div data-docs data-page="<%= @documentation_page %>" data-resource="<%= escape_html(@documentation_resource && @documentation_resource[:key]) %>">
4
- <a class="docs-skip-link" href="#docs-main">Skip to content</a>
4
+ <a class="docs-skip-link" href="#docs-main"><%= escape_html(documentation_t('accessibility.skip')) %></a>
5
5
  <div class="docs-mobile-toolbar">
6
- <button type="button" data-nav-toggle aria-controls="docs-navigation" aria-expanded="false">Navigation</button>
6
+ <button type="button" data-nav-toggle aria-controls="docs-navigation" aria-expanded="false"><%= escape_html(documentation_t('accessibility.navigation')) %></button>
7
7
  </div>
8
8
  <div class="docs-layout">
9
9
  <aside class="docs-sidebar" id="docs-navigation" data-docs-sidebar><%= @sidebar %></aside>
10
10
  <main class="docs-main" id="docs-main" tabindex="-1"><%= @content %></main>
11
11
  </div>
12
+ <script type="application/json" data-documentation-messages><%= documentation_json(documentation_t('browser')) %></script>
12
13
  <script type="application/json" data-documentation-clients><%= documentation_json(@help ? documentation_client_metadata : []) %></script>
13
14
  <script type="application/json" data-legacy-anchors><%= documentation_json(@documentation_page == 'overview' ? documentation_legacy_anchors : {}) %></script>
14
15
  </div>
@@ -1,20 +1,20 @@
1
- <h2>Contents</h2>
1
+ <h2><%= escape_html(documentation_t('labels.contents')) %></h2>
2
2
  <ul class="top-level">
3
- <li><a href="#client-definition">Client definition</a></li>
4
- <li><a href="#design-rules">Design rules</a></li>
3
+ <li><a href="#client-definition"><%= escape_html(documentation_t('toc.client_definition')) %></a></li>
4
+ <li><a href="#design-rules"><%= escape_html(documentation_t('toc.design_rules')) %></a></li>
5
5
  <li>
6
- <a href="#necessary-features-to-implement">Necessary features to implement</a>
6
+ <a href="#necessary-features-to-implement"><%= escape_html(documentation_t('toc.necessary_features_to_implement')) %></a>
7
7
  <ul>
8
- <li><a href="#resource-tree">Resource tree</a></li>
9
- <li><a href="#input-output-parameters">Input/output parameters</a></li>
10
- <li><a href="#authentication">Authentication</a></li>
11
- <li><a href="#object-like-access">Object-like access</a></li>
8
+ <li><a href="#resource-tree"><%= escape_html(documentation_t('toc.resource_tree')) %></a></li>
9
+ <li><a href="#input-output-parameters"><%= escape_html(documentation_t('toc.input_output_parameters')) %></a></li>
10
+ <li><a href="#authentication"><%= escape_html(documentation_t('labels.authentication')) %></a></li>
11
+ <li><a href="#object-like-access"><%= escape_html(documentation_t('toc.object_like_access')) %></a></li>
12
12
  </ul>
13
13
  </li>
14
14
  <li>
15
- <a href="#supplemental-features">Supplemental features</a>
15
+ <a href="#supplemental-features"><%= escape_html(documentation_t('toc.supplemental_features')) %></a>
16
16
  <ul>
17
- <li><a href="#client-side-validations">Client-side validations</a></li>
17
+ <li><a href="#client-side-validations"><%= escape_html(documentation_t('toc.client_side_validations')) %></a></li>
18
18
  </ul>
19
19
  </li>
20
20
  </ul>
@@ -1,6 +1,6 @@
1
- <h2>Contents</h2>
1
+ <h2><%= escape_html(documentation_t('labels.contents')) %></h2>
2
2
  <ul class="top-level">
3
- <li><a href="#envelope">Envelope</a></li>
3
+ <li><a href="#envelope"><%= escape_html(documentation_t('schema.envelope')) %></a></li>
4
4
  <li><a href="#all">OPTIONS /</a></li>
5
5
  <li><a href="#versions">OPTIONS /?describe=versions</a></li>
6
6
  <li><a href="#default">OPTIONS /?describe=default</a></li>
@@ -1,42 +1,42 @@
1
- <h2>Contents</h2>
1
+ <h2><%= escape_html(documentation_t('labels.contents')) %></h2>
2
2
  <ul class="top-level">
3
- <li><a href="#protocol-definition">Protocol definition</a></li>
4
- <li><a href="#self-description">Self-description</a></li>
5
- <li><a href="#envelope">Envelope</a></li>
3
+ <li><a href="#protocol-definition"><%= escape_html(documentation_t('toc.protocol_definition')) %></a></li>
4
+ <li><a href="#self-description"><%= escape_html(documentation_t('toc.self_description')) %></a></li>
5
+ <li><a href="#envelope"><%= escape_html(documentation_t('schema.envelope')) %></a></li>
6
6
  <li>
7
- <a href="#description-format">Description format</a>
7
+ <a href="#description-format"><%= escape_html(documentation_t('toc.description_format')) %></a>
8
8
  <ul>
9
- <li><a href="#api-version">Version</a></li>
9
+ <li><a href="#api-version"><%= escape_html(documentation_t('toc.version')) %></a></li>
10
10
  <li>
11
- <a href="#authentication">Authentication</a>
11
+ <a href="#authentication"><%= escape_html(documentation_t('labels.authentication')) %></a>
12
12
  <ul>
13
- <li><a href="#http-basic-authentication">HTTP basic authentication</a></li>
14
- <li><a href="#token-authentication">Token authentication</a></li>
13
+ <li><a href="#http-basic-authentication"><%= escape_html(documentation_t('toc.http_basic_authentication')) %></a></li>
14
+ <li><a href="#token-authentication"><%= escape_html(documentation_t('toc.token_authentication')) %></a></li>
15
15
  </ul>
16
16
  </li>
17
- <li><a href="#resources">Resources</a></li>
17
+ <li><a href="#resources"><%= escape_html(documentation_t('labels.resources')) %></a></li>
18
18
  <li>
19
- <a href="#actions">Actions</a>
19
+ <a href="#actions"><%= escape_html(documentation_t('labels.actions')) %></a>
20
20
  <ul>
21
- <li><a href="#layouts">Layouts</a></li>
22
- <li><a href="#namespace">Namespace</a></li>
21
+ <li><a href="#layouts"><%= escape_html(documentation_t('toc.layouts')) %></a></li>
22
+ <li><a href="#namespace"><%= escape_html(documentation_t('labels.namespace')) %></a></li>
23
23
  </ul>
24
24
  </li>
25
25
  <li>
26
- <a href="#parameters">Parameters</a>
26
+ <a href="#parameters"><%= escape_html(documentation_t('toc.parameters')) %></a>
27
27
  <ul>
28
- <li><a href="#data-types">Data types</a></li>
29
- <li><a href="#resource-association">Resource associations</a></li>
28
+ <li><a href="#data-types"><%= escape_html(documentation_t('toc.data_types')) %></a></li>
29
+ <li><a href="#resource-association"><%= escape_html(documentation_t('toc.resource_associations')) %></a></li>
30
30
  </ul>
31
31
  </li>
32
- <li><a href="#examples">Examples</a></li>
33
- <li><a href="#list-api-versions">List API versions</a></li>
34
- <li><a href="#describe-default-version">Describe default version</a></li>
35
- <li><a href="#describe-the-whole-api">Describe the whole API</a></li>
32
+ <li><a href="#examples"><%= escape_html(documentation_t('labels.examples')) %></a></li>
33
+ <li><a href="#list-api-versions"><%= escape_html(documentation_t('toc.list_api_versions')) %></a></li>
34
+ <li><a href="#describe-default-version"><%= escape_html(documentation_t('toc.describe_default_version')) %></a></li>
35
+ <li><a href="#describe-the-whole-api"><%= escape_html(documentation_t('toc.describe_the_whole_api')) %></a></li>
36
36
  </ul>
37
37
  </li>
38
- <li><a href="#authorization">Authorization</a></li>
39
- <li><a href="#input-output-formats">Input/output formats</a></li>
40
- <li><a href="#request">Request</a></li>
41
- <li><a href="#response">Response</a></li>
38
+ <li><a href="#authorization"><%= escape_html(documentation_t('toc.authorization')) %></a></li>
39
+ <li><a href="#input-output-formats"><%= escape_html(documentation_t('toc.input_output_formats')) %></a></li>
40
+ <li><a href="#request"><%= escape_html(documentation_t('toc.request')) %></a></li>
41
+ <li><a href="#response"><%= escape_html(documentation_t('toc.response')) %></a></li>
42
42
  </ul>
@@ -1,9 +1,10 @@
1
1
  <form data-documentation-search method="get" action="<%= escape_html(action) %>" data-search-version="<%= escape_html(version) %>" data-search-index-url="<%= escape_html(index_url) %>" data-search-view-key="<%= escape_html(view_key) %>">
2
2
  <input type="hidden" name="page" value="resources">
3
- <label for="<%= escape_html(id) %>-query">Search API v<%= escape_html(version) %></label>
4
- <input type="search" name="q" data-search-query id="<%= escape_html(id) %>-query" value="<%= escape_html(query) %>" placeholder="Resource..." maxlength="256">
5
- <button type="submit">Search</button>
6
- <div data-search-suggestions role="region" aria-label="Search suggestions" hidden>
3
+ <input type="hidden" name="doc_lang" value="<%= escape_html(documentation_locale) %>">
4
+ <label for="<%= escape_html(id) %>-query"><%= escape_html(documentation_t('search.label', version: version)) %></label>
5
+ <input type="search" name="q" data-search-query id="<%= escape_html(id) %>-query" value="<%= escape_html(query) %>" placeholder="<%= escape_html(documentation_t('search.placeholder')) %>" maxlength="256">
6
+ <button type="submit"><%= escape_html(documentation_t('search.submit')) %></button>
7
+ <div data-search-suggestions role="region" aria-label="<%= escape_html(documentation_t('search.suggestions')) %>" hidden>
7
8
  <p data-search-status role="status"></p>
8
9
  <div data-search-preview></div>
9
10
  </div>
@@ -1,18 +1,18 @@
1
- <section data-search-results aria-label="Search results">
2
- <h2>Search results for <%= escape_html(search[:query]) %></h2>
1
+ <section data-search-results aria-label="<%= escape_html(documentation_t('search.results')) %>">
2
+ <h2><%= escape_html(documentation_t('search.results_for', query: search[:query])) %></h2>
3
3
  <p data-search-status role="status">
4
4
  <% if search[:results].empty? %>
5
- No matches. Try another resource, action or parameter name.
5
+ <%= escape_html(documentation_t('browser.search_empty')) %>
6
6
  <% elsif search[:more] %>
7
- Showing <%= search[:results].length %> results. More matches are available; refine your search.
7
+ <%= escape_html(documentation_t('browser.search_more', count: search[:results].length)) %>
8
8
  <% else %>
9
- <%= search[:results].length %> <%= search[:results].length == 1 ? 'result' : 'results' %>.
9
+ <%= escape_html(documentation_t(search[:results].length == 1 ? 'browser.search_one' : 'browser.search_count', count: search[:results].length)) %>
10
10
  <% end %>
11
11
  </p>
12
12
  <ol>
13
13
  <% search[:results].each do |result| %>
14
14
  <li>
15
- <a href="<%= escape_html(documentation_search_result_url(result)) %>"><%= escape_html(result[:kind].to_s.capitalize) %>: <code><%= escape_html(documentation_search_result_title(result)) %></code></a>
15
+ <a href="<%= escape_html(documentation_search_result_url(result)) %>"><%= escape_html(documentation_t("browser.#{result[:kind]}")) %>: <code><%= escape_html(documentation_search_result_title(result)) %></code></a>
16
16
  <% documentation_search_result_context(result).each do |context| %><span><%= escape_html(context) %></span><% end %>
17
17
  </li>
18
18
  <% end %>
@@ -1,10 +1,10 @@
1
- <h1>API versions</h1>
1
+ <h1><%= escape_html(documentation_t('labels.api_versions')) %></h1>
2
2
  <ul>
3
3
  <% @api[:versions].each do |v, info| %>
4
4
  <% next if v == :default %>
5
5
  <li>
6
- <a href="<%= escape_html(url(info[:help], false)) %>">Version <%= escape_html(v) %></a>
7
- <%= '(default)' if v == @api[:default_version] %>
6
+ <a href="<%= escape_html(url(info[:help], false)) %>"><%= escape_html(documentation_t('version.label', version: v)) %></a>
7
+ <%= escape_html(documentation_t('version.default')) if v == @api[:default_version] %>
8
8
  </li>
9
9
  <% end %>
10
10
  </ul>