seiten 1.1.0 → 1.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f11b589f94032def9f0ef35b5fd457e1acb0f3508cf39f1ac63fb0338ee8217a
4
- data.tar.gz: 71e0543f85e054dac2cf348e6e9ecf1eebde83d1659921dca8c4460a734665b6
3
+ metadata.gz: 6289826464b8f2c34c78a4d469fe26e0e772c79f67adf29f38dc3465c9d35ed6
4
+ data.tar.gz: fb131648fd8558fadf4e3b17f27aa1c24e4b4d4b59a13fcf0d173632c561c2a3
5
5
  SHA512:
6
- metadata.gz: 186fe751f440f15e30944acc416a9dfac931bcf1afd0f65c4f58ba5b6e7c8a0ebd3fab9e8ad39260c39aebc26de7769cb5b829c4b05c7f35c30407806a97577a
7
- data.tar.gz: 133f9dd5ac075f2805088865e309183c453a2e889b0cc090c970db1e94076e603f4a60a8f249685b97d674741b857a6e78112ce33f356c0e3aaaa64e73b1d8b5
6
+ metadata.gz: b69aa165a103bd083dba0cb57c6c34078b139d56dba5a0e83679e4d654bd863e1e245d7dc247e429f6f3bf393818faa450f6c59fe9ad2255498e773427eb865c
7
+ data.tar.gz: '0643406590aa1ae9a182222163f903764807699d460cfa9652f3a56e235685bbb4c50e440701cae5a519f1f0a2afb5c5ba680ea41d74bf9098257737bd39242b'
@@ -32,6 +32,16 @@ module Seiten
32
32
  page_collection
33
33
  end
34
34
 
35
+ # The navigation's own title and slug, from I18n like its pages' (<i18n_scope>.<name>.title and
36
+ # .slug; the slug defaults to the parameterized title). Its localized routes start with the slug.
37
+ def title
38
+ I18n.t('title', scope: [Seiten.config[:i18n_scope], name], locale: locale, default: nil)
39
+ end
40
+
41
+ def slug
42
+ I18n.t('slug', scope: [Seiten.config[:i18n_scope], name], locale: locale, default: nil) || title&.parameterize
43
+ end
44
+
35
45
  def pages=(pages_array)
36
46
  page_collection.pages = pages_array.map { |page| page.navigation_id = id; page }
37
47
  # return page_collection
@@ -3,5 +3,8 @@ module Seiten
3
3
  config.action_dispatch.rescue_responses.merge!(
4
4
  'Seiten::Errors::RoutingError' => :not_found
5
5
  )
6
+
7
+ # Navigations built from I18n follow reloaded translations in development.
8
+ config.to_prepare { Seiten.reset_i18n_navigations! }
6
9
  end
7
10
  end
@@ -1,15 +1,48 @@
1
1
  class ActionDispatch::Routing::Mapper
2
+ # Routes a navigation's pages. Draw it after your other routes, which it would swallow.
3
+ #
4
+ # A navigation built in every locale from one file (config/navigations/help.yml) gets a route per
5
+ # locale, under the locale and the navigation's slug (its I18n title or slug, see
6
+ # Navigation#slug), to Help::PagesController#show, named help_page_<locale>:
7
+ #
8
+ # seiten :help # /en/help(/*slug), /de/hilfe(/*slug)
9
+ #
10
+ # Every page gets a link helper named after the navigation and its key, and the navigation one that
11
+ # takes a key; both link to the page in I18n.locale or the `locale:` option:
12
+ #
13
+ # help_account_manage_path # => "/en/help/your-account/manage-your-account"
14
+ # help_account_manage_url(locale: :de) # => "http://…/de/hilfe/dein-account/account-verwalten"
15
+ # help_page_path(page.key) # the same, for a key in a variable; unknown keys raise
16
+ # help_page_path # => "/en/help", the navigation's root
17
+ #
18
+ # `to:`, `as:` (of the navigation's helper) and `path:` (the same in every locale) replace the
19
+ # defaults, `prefix: false` names the page helpers without the navigation (account_manage_path), and
20
+ # `localized: false` draws one catch-all route instead.
21
+ #
22
+ # Other navigations get one catch-all route to seiten/pages#show (or `to:`) that sets
23
+ # params[:navigation_id] (except for the application navigation); `localized: true` routes them per
24
+ # locale too.
25
+ #
26
+ # seiten # /*slug, the application navigation
27
+ # scope(:help) { seiten :help }
2
28
  def seiten(*resources)
3
29
  options = resources.extract_options!
4
- options[:to] ||= 'seiten/pages#show'
30
+ localized, path, prefix = options.extract!(:localized, :path, :prefix).values_at(:localized, :path, :prefix)
5
31
 
6
32
  resources.each do |resource|
7
- resource_options = options.dup
33
+ name = resource.to_s
34
+
35
+ if localized.nil? ? Seiten.i18n_navigation?(name) : localized
36
+ seiten_localized_routes(name, options.dup, path: path, prefix: prefix.nil? ? name : prefix)
37
+ next
38
+ end
8
39
 
40
+ resource_options = options.dup
41
+ resource_options[:to] ||= 'seiten/pages#show'
9
42
  resource_options[:as] ||= resource == :application ? :seiten_page : "seiten_#{resource}_page"
10
43
 
11
- resource_options[:defaults] ||= {}
12
- resource_options[:defaults][:navigation_id] = resource.to_s unless resource == :application
44
+ resource_options[:defaults] = resource_options[:defaults].to_h.dup
45
+ resource_options[:defaults][:navigation_id] = name unless resource == :application
13
46
  resource_options[:defaults][:slug] = ''
14
47
 
15
48
  # NOTE: See https://github.com/rails/rails/issues/31228
@@ -18,4 +51,46 @@ class ActionDispatch::Routing::Mapper
18
51
  get '(*slug)', **resource_options
19
52
  end
20
53
  end
54
+
55
+ private
56
+
57
+ def seiten_localized_routes(name, options, path:, prefix:)
58
+ navigations = Seiten::Navigation.where(name: name)
59
+ raise ArgumentError, "No #{name} navigation in #{Seiten.config[:config_dir]}" if navigations.empty?
60
+
61
+ route = (options.delete(:as) || "#{name}_page").to_s
62
+ options = { to: "#{name}/pages#show", format: false, **options }
63
+ defaults = { **options.delete(:defaults).to_h, navigation_id: name, slug: '' }
64
+
65
+ navigations.each do |navigation|
66
+ locale_path = [navigation.locale, path || navigation.slug].compact_blank.join('/')
67
+ get "#{locale_path}(/*slug)", **options, as: "#{route}_#{navigation.locale}", defaults: defaults.merge(locale: navigation.locale)
68
+ end
69
+
70
+ keys = navigations.first.pages.all.filter_map(&:key)
71
+ helpers = keys.map { |key| [prefix.presence, *key.split('.')].compact.join('_') }
72
+ duplicate = [route, *helpers].tally.find { |_, count| count > 1 }&.first
73
+ raise ArgumentError, "The #{name} navigation would define #{duplicate}_path twice" if duplicate
74
+
75
+ seiten_link_helper(route, name, route)
76
+ keys.zip(helpers).each { |key, helper| seiten_link_helper(helper, name, route, key) }
77
+ end
78
+
79
+ # <helper>_path and <helper>_url: the page with `page_key` (else the key given first, else the
80
+ # navigation's root) in the navigation of I18n.locale or the `locale:` option.
81
+ def seiten_link_helper(helper, navigation_name, route, page_key = nil)
82
+ @set.add_url_helper(helper, {}) do |*args|
83
+ options = args.extract_options!
84
+ key = page_key || args.first
85
+ locale = (options[:locale] || I18n.locale).to_s
86
+
87
+ navigation = Seiten::Navigation.find_by(name: navigation_name, locale: locale)
88
+ raise ArgumentError, "No #{navigation_name} navigation in #{locale}" unless navigation
89
+
90
+ page = navigation.pages.find_by(key: key.to_s) if key
91
+ raise ArgumentError, "No page #{key} in the #{navigation_name} navigation" if key && !page
92
+
93
+ { use_route: "#{route}_#{locale}", slug: page&.slug.to_s, **options.except(:locale) }
94
+ end
95
+ end
21
96
  end
@@ -1,3 +1,3 @@
1
1
  module Seiten
2
- VERSION = "1.1.0"
2
+ VERSION = "1.3.0"
3
3
  end
data/lib/seiten.rb CHANGED
@@ -21,6 +21,7 @@ module Seiten
21
21
  config_dir: File.join('config', 'navigations'),
22
22
  pages_dir: File.join('app', 'pages'),
23
23
  root_page: 'home',
24
+ i18n_scope: 'navigations',
24
25
  default_layout: 'application',
25
26
  html: {
26
27
  navigation: {
@@ -45,6 +46,9 @@ module Seiten
45
46
  }
46
47
 
47
48
  @navigations = []
49
+ @i18n_files = {}
50
+ @i18n_navigations = nil
51
+ @i18n_lock = Mutex.new
48
52
 
49
53
  class << self
50
54
  def config
@@ -52,6 +56,7 @@ module Seiten
52
56
  end
53
57
 
54
58
  def navigations
59
+ build_i18n_navigations unless @i18n_navigations
55
60
  @navigations
56
61
  end
57
62
 
@@ -59,14 +64,72 @@ module Seiten
59
64
  @navigations = navigations
60
65
  end
61
66
 
67
+ # config/navigations/<name>.<locale>.yml holds one locale's navigation; <name>.yml holds the pages
68
+ # of every locale, with titles and slugs from I18n (built on first use, see #i18n_pages).
62
69
  def initialize_navigations
63
70
  Dir[File.join(Rails.root, Seiten.config[:config_dir], "*.yml")].each do |file|
64
- id = File.basename(file, '.yml')
65
- name = id.gsub(/\..*/, '')
66
- locale = id.gsub(/.*\./, '')
67
- navigation = Seiten::Navigation.new(name: name, locale: locale, dir: File.join(Rails.root, Seiten.config[:pages_dir], name, locale))
68
- navigation.page_collection.build(pages: YAML.load_file(navigation.config))
69
- Seiten.navigations << navigation
71
+ id = File.basename(file, '.yml')
72
+ if id.include?('.')
73
+ name, locale = id.split('.', 2)
74
+ navigation = Seiten::Navigation.new(name: name, locale: locale, dir: File.join(Rails.root, Seiten.config[:pages_dir], name, locale))
75
+ navigation.page_collection.build(pages: YAML.load_file(navigation.config))
76
+ @navigations << navigation
77
+ else
78
+ @i18n_files[id] = file
79
+ end
80
+ end
81
+ end
82
+
83
+ # Whether the navigation is built in every locale from one file (config/navigations/<name>.yml).
84
+ def i18n_navigation?(name)
85
+ @i18n_files.key?(name.to_s)
86
+ end
87
+
88
+ # Drops the navigations built from I18n, so they're built again with the current translations
89
+ # (on every code or translation reload in development).
90
+ def reset_i18n_navigations!
91
+ @i18n_lock.synchronize do
92
+ @navigations -= @i18n_navigations if @i18n_navigations
93
+ @i18n_navigations = nil
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def build_i18n_navigations
100
+ @i18n_lock.synchronize do
101
+ return if @i18n_navigations
102
+
103
+ @i18n_navigations = @i18n_files.flat_map do |name, file|
104
+ structure = YAML.load_file(file)
105
+ I18n.available_locales.map do |locale|
106
+ navigation = Seiten::Navigation.new(name: name, locale: locale, config: file, dir: File.join(Rails.root, Seiten.config[:pages_dir], name, locale.to_s))
107
+ navigation.page_collection.build(pages: i18n_pages(structure, locale, [Seiten.config[:i18n_scope], name]))
108
+ navigation
109
+ end
110
+ end
111
+ @navigations.concat(@i18n_navigations)
112
+ end
113
+ end
114
+
115
+ # The pages of a locale: every page names its `key`; its title and slug are the translations
116
+ # <i18n_scope>.<navigation>.<parent keys…>.<key>.title and .slug (the slug defaults to the
117
+ # parameterized title). The page's key becomes its full key, e.g. "account.manage", which is the
118
+ # same page in every locale.
119
+ def i18n_pages(nodes, locale, scope)
120
+ nodes.map do |node|
121
+ node = node.dup
122
+ key = node['key'] or raise Errors::PageError, "Every page of #{scope[1]}.yml needs a key"
123
+ page_scope = [*scope, key.to_s]
124
+ title = I18n.t('title', scope: page_scope, locale: locale, default: nil)
125
+ slug = I18n.t('slug', scope: page_scope, locale: locale, default: nil)
126
+ raise Errors::PageError, "#{page_scope.join('.')} needs a title or a slug in #{locale}" unless title || slug || node.key?('url')
127
+
128
+ node['title'] = title
129
+ node['url'] = slug if slug && !node.key?('url')
130
+ node['key'] = page_scope.drop(2).join('.')
131
+ node['nodes'] = i18n_pages(node['nodes'], locale, page_scope) if node['nodes']
132
+ node
70
133
  end
71
134
  end
72
135
  end
@@ -0,0 +1,5 @@
1
+ class Guide::PagesController < ApplicationController
2
+ def show
3
+ render plain: "#{current_navigation.id} #{current_page&.key}"
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ de:
2
+ navigations:
3
+ guide:
4
+ title: Anleitung
5
+ start:
6
+ title: Start
7
+ account:
8
+ title: Dein Account
9
+ manage:
10
+ title: Account verwalten
11
+ security:
12
+ title: Sicherheit
13
+ billing:
14
+ title: Rechnungen
@@ -0,0 +1,15 @@
1
+ en:
2
+ navigations:
3
+ guide:
4
+ title: Guide
5
+ start:
6
+ title: Start
7
+ account:
8
+ title: Your account
9
+ manage:
10
+ title: Manage your account
11
+ security:
12
+ title: Security
13
+ slug: account-security
14
+ billing:
15
+ title: Billing
@@ -0,0 +1,7 @@
1
+ - key: start
2
+ url: '/'
3
+ - key: account
4
+ nodes:
5
+ - key: manage
6
+ - key: security
7
+ - key: billing
@@ -3,6 +3,8 @@ Dummy::Application.routes.draw do
3
3
 
4
4
  get "/blog", to: "posts#index"
5
5
 
6
+ seiten :guide
7
+
6
8
  scope :help do
7
9
  seiten :help
8
10
  end
File without changes