isola 0.1.3 → 0.2.1

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: 2bb22047ab87f7c563e5970881ccb38fc9efd2a2f6330dacdb6d59f7d8b45692
4
- data.tar.gz: 361457eac4fe0368b586258f36b02a9a7748e64be5f31f6cc83acb4ed7bf6ee1
3
+ metadata.gz: c9ea7b839acee149cdc001dae2606001e8393efebafb19bcbea8852a694bc7a0
4
+ data.tar.gz: 95435cd787ea492043c2b8f0f62b8585e3265f4e7d5f6f315e1705e322700938
5
5
  SHA512:
6
- metadata.gz: e4bc2dada021f9fddda63594d32fccbe2d9d25040340344632c1efedf9319fd13113049b95e04f29d1c1f81dee39489bbd2ea4221119af2e7f28414cb4d724a0
7
- data.tar.gz: b19a681a05704dd1223e6914cdede398229e6f4b6e727169557a19ecea77f49aff0e7ea59f12718a7954e8013ec02410eb5ad8dbea0b59d5bad30d8879aef6ce
6
+ metadata.gz: e80d3f35ca2f20a8d449fd224e2b3f580c3d32161e3d8ec0259711fe5be6438f28d5f0c89dc663c693e42283b3d1673916c0ad7fe28620f444d7f77f9a30d297
7
+ data.tar.gz: 289d16edbc4e8c3e9762015e6088a15e34a88af9c4874ddeba90bee78c2457cdabf15708fcc81c1058b468732022941f0f5b99c081557f6032dc1c3ae57ab416
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ inherit_gem:
2
+ standard: config/base.yml
data/USAGE.ja.md CHANGED
@@ -116,3 +116,67 @@ Isolaが処理するテンプレートエンジンは現在 **ERB**(`.erb`)
116
116
  | `*.css`, `*.js` など | なし | そのままコピー |
117
117
 
118
118
  `_`や`.`で始まるファイル・ディレクトリは自動的に除外されます(`_layouts/`と`_includes/`を除く)。
119
+
120
+ ## 多言語対応
121
+
122
+ Isolaは多言語サイトの構築をサポートしています。デフォルト言語のページはサイトルートに、その他の言語は言語コード名のサブディレクトリに配置します。
123
+
124
+ ### 設定
125
+
126
+ `_config.yaml`に`default_language`と`languages`を追加します:
127
+
128
+ ```yaml
129
+ default_language: ja
130
+ languages:
131
+ ja:
132
+ label: "日本語"
133
+ en:
134
+ label: "English"
135
+ title: "My Site (EN)"
136
+ ```
137
+
138
+ 各言語エントリでサイトレベルの設定値を上書きできます。例えば上記の設定では、英語ページのレンダリング時に`site[:title]`が`"My Site (EN)"`を返します。
139
+
140
+ ### ディレクトリ構成
141
+
142
+ ```
143
+ my-site/
144
+ ├── _config.yaml
145
+ ├── _layouts/
146
+ │ ├── default.html.erb # 共有レイアウト(全言語で使用)
147
+ │ └── en/default.html.erb # 英語専用レイアウト(上書き)
148
+ ├── _includes/
149
+ │ ├── head.html.erb # 共有インクルード
150
+ │ └── en/head.html.erb # 英語専用インクルード(上書き)
151
+ ├── index.md # デフォルト言語(ja)のページ
152
+ └── en/index.md # 英語ページ
153
+ ```
154
+
155
+ - **ページ**: デフォルト言語のページはルートに配置します。他の言語は`<lang>/`サブディレクトリに配置します(例: `en/index.md`)。
156
+ - **レイアウトとインクルード**: 言語固有のオーバーライドは`_layouts/<lang>/`や`_includes/<lang>/`に配置します。言語固有のバージョンが見つからない場合、共有バージョンがフォールバックとして使われます。
157
+
158
+ ### テンプレート変数
159
+
160
+ 標準のテンプレート変数に加え、多言語サイトでは以下が利用できます:
161
+
162
+ - `page[:lang]` — 現在のページの言語(例: `:ja`, `:en`)
163
+ - `page[:translations]` — 現在のページの全翻訳版を`{lang: url_path}`形式のハッシュで返す(先頭が`/`のURLパスであり、`href`属性などにそのまま利用できます)
164
+
165
+ #### hreflangリンクの生成
166
+
167
+ `page[:translations]`を使って代替言語リンクを出力できます:
168
+
169
+ ```erb
170
+ <% page[:translations].each do |lang, url| %>
171
+ <link rel="alternate" hreflang="<%= lang %>" href="<%= url %>">
172
+ <% end %>
173
+ ```
174
+
175
+ ### 出力
176
+
177
+ 出力はソースの構成を反映します:
178
+
179
+ | ソース | 出力 |
180
+ |---|---|
181
+ | `index.md` | `_site/index.html` |
182
+ | `en/index.md` | `_site/en/index.html` |
data/USAGE.md CHANGED
@@ -116,3 +116,67 @@ If an extension remains after processing, it is kept as-is. If no extension rema
116
116
  | `*.css`, `*.js` etc. | None | Copied as-is |
117
117
 
118
118
  Files and directories starting with `_` or `.` are excluded automatically (except `_layouts/` and `_includes/`).
119
+
120
+ ## Multi-Language Support
121
+
122
+ Isola supports building multi-language sites. Pages for the default language live at the site root, while other languages are placed in subdirectories named by language code.
123
+
124
+ ### Configuration
125
+
126
+ Add `default_language` and `languages` to `_config.yaml`:
127
+
128
+ ```yaml
129
+ default_language: ja
130
+ languages:
131
+ ja:
132
+ label: "日本語"
133
+ en:
134
+ label: "English"
135
+ title: "My Site (EN)"
136
+ ```
137
+
138
+ Each language entry can override any site-level configuration value. For example, `site[:title]` returns `"My Site (EN)"` when rendering English pages.
139
+
140
+ ### Directory Structure
141
+
142
+ ```
143
+ my-site/
144
+ ├── _config.yaml
145
+ ├── _layouts/
146
+ │ ├── default.html.erb # Shared layout (used by all languages)
147
+ │ └── en/default.html.erb # English-specific layout override
148
+ ├── _includes/
149
+ │ ├── head.html.erb # Shared include
150
+ │ └── en/head.html.erb # English-specific include override
151
+ ├── index.md # Default language (ja) page
152
+ └── en/index.md # English page
153
+ ```
154
+
155
+ - **Pages**: Default-language pages live at the root. Other languages go in `<lang>/` subdirectories (e.g. `en/index.md`).
156
+ - **Layouts and includes**: Place language-specific overrides under `_layouts/<lang>/` or `_includes/<lang>/`. If a language-specific version is not found, the shared version is used as a fallback.
157
+
158
+ ### Template Variables
159
+
160
+ In addition to the standard template variables, multi-language sites provide:
161
+
162
+ - `page[:lang]` — the language of the current page (e.g. `:ja`, `:en`)
163
+ - `page[:translations]` — a hash of `{lang: url_path}` (URL paths starting with `/`, suitable for `href` attributes) for all available translations of the current page
164
+
165
+ #### Generating hreflang Links
166
+
167
+ Use `page[:translations]` to output alternate-language links:
168
+
169
+ ```erb
170
+ <% page[:translations].each do |lang, url| %>
171
+ <link rel="alternate" hreflang="<%= lang %>" href="<%= url %>">
172
+ <% end %>
173
+ ```
174
+
175
+ ### Output
176
+
177
+ The output mirrors the source structure:
178
+
179
+ | Source | Output |
180
+ |---|---|
181
+ | `index.md` | `_site/index.html` |
182
+ | `en/index.md` | `_site/en/index.html` |
data/lib/isola/context.rb CHANGED
@@ -2,21 +2,36 @@ require "tilt"
2
2
 
3
3
  module Isola
4
4
  class Context
5
- attr_reader :site, :content, :layout
6
- def initialize(source, site)
5
+ attr_reader :content, :layout
6
+ def initialize(source, site, languages: {})
7
7
  @source = source
8
- @meta = {lang: site[:lang]}.merge(source.meta).freeze
8
+ @meta = source.meta.freeze
9
9
  @site = site
10
10
  @content = ""
11
11
  @layout = {}
12
+
13
+ site_config = @site.config.merge(languages[@source[:lang]] || {})
14
+ @site_proxy = SimpleDelegator.new(@site).tap do
15
+ it.define_singleton_method(:[]) do |key|
16
+ site_config[key]
17
+ end
18
+ end
12
19
  end
13
20
 
14
21
  def page
15
22
  @meta
16
23
  end
17
24
 
25
+ def site
26
+ @site_proxy
27
+ end
28
+
29
+ def lang_path path
30
+ @site_proxy.url_path_for_lang(path, @source[:lang])
31
+ end
32
+
18
33
  def include name, params = {}
19
- i = @site.include name
34
+ i = @site.include name, lang: @source[:lang]
20
35
  raise "include #{name} not found in #{@current.filepath}" unless i
21
36
  i.render(self, @site, params)[0]
22
37
  end
@@ -25,7 +40,7 @@ module Isola
25
40
  @current = @source
26
41
  @content, path = @source.render(self, @site)
27
42
  while @current.meta[:layout]
28
- layout = site.layout(@current.meta[:layout])
43
+ layout = site.layout(@current.meta[:layout], lang: @source[:lang])
29
44
  raise "#{@current.meta[:layout]} not found for #{@current.filepath}" unless layout
30
45
  @current = layout
31
46
  @content, _ = @current.render(self, @site)
@@ -0,0 +1,39 @@
1
+ module Isola
2
+ class LanguagePathRouter
3
+ def initialize(default_language:, languages:)
4
+ @default_language = default_language.to_s
5
+ @languages = languages.map(&:to_s)
6
+ end
7
+
8
+ def language_for(path)
9
+ p = Pathname(path).each_filename.to_a
10
+ if p.length > 1 && @languages.include?(p[0])
11
+ p[0].to_sym
12
+ else
13
+ @default_language.to_sym
14
+ end
15
+ end
16
+
17
+ def canonical_path(path)
18
+ p = Pathname(path).each_filename.to_a
19
+ if p.length > 1 && @languages.include?(p[0])
20
+ File.join(p[1..])
21
+ else
22
+ path
23
+ end
24
+ end
25
+
26
+ def localized_path(path, language)
27
+ canonical = canonical_path(path)
28
+ if language.to_s == @default_language
29
+ canonical
30
+ else
31
+ File.join(language.to_s, canonical)
32
+ end
33
+ end
34
+
35
+ def candidate_paths(path)
36
+ @languages.to_h { |lang| [lang.to_sym, localized_path(path, lang)] }
37
+ end
38
+ end
39
+ end
data/lib/isola/site.rb CHANGED
@@ -1,30 +1,47 @@
1
1
  require "yaml"
2
2
  require "fileutils"
3
+ require "delegate"
4
+
3
5
  module Isola
4
6
  class Site
5
7
  attr_accessor :config
6
8
  DEFAULT_CONFIG = {url: "http://example.com",
7
9
  title: "my awesome site",
8
10
  destination: "_site",
9
- default_language: "en",
11
+ default_language: :en,
10
12
  host: "127.0.0.1",
13
+ languages: {},
11
14
  port: 4444}.freeze
12
15
  SUPPORTED_TILT_EXT = [".erb", ".md", ".markdown", ".mkd", ".html"]
13
16
  EXT_MAP = {".md" => ".html", ".mkd" => ".html", ".markdown" => ".html", ".html" => ".html", "" => ".html"}
14
17
  def initialize(config)
15
18
  @config = DEFAULT_CONFIG.merge(YAML.safe_load(config, symbolize_names: true) || {})
19
+ @config[:default_language] = @config[:default_language].to_sym
16
20
  @config[:root_dir] ||= Dir.pwd
17
21
  @config[:excludes] ||= []
22
+ @lang_router = LanguagePathRouter.new(
23
+ default_language: default_language,
24
+ languages: languages.keys
25
+ )
18
26
  collect_files
19
27
  end
20
28
 
21
29
  def [] key
22
- if key == :lang
23
- key = :default_language
24
- end
25
30
  @config[key]
26
31
  end
27
32
 
33
+ def default_language
34
+ @config[:default_language]
35
+ end
36
+
37
+ def languages
38
+ @config[:languages]
39
+ end
40
+
41
+ def language_label(lang)
42
+ @config[:languages][lang].to_h[:label]
43
+ end
44
+
28
45
  def ext_to_process_with_tilt? ext
29
46
  SUPPORTED_TILT_EXT.include? ext
30
47
  end
@@ -58,16 +75,25 @@ module Isola
58
75
  build
59
76
  end
60
77
 
78
+ def url_path_for(path)
79
+ # will support base_url for the future.
80
+ File.join("/", path)
81
+ end
82
+
83
+ def url_path_for_lang(path, lang)
84
+ url_path_for(@lang_router.localized_path(path, lang))
85
+ end
86
+
61
87
  def ignore?(path)
62
88
  @file_handler.ignore?(path)
63
89
  end
64
90
 
65
- def layout name
66
- find_entry(name, @parsed_layouts, @file_handler.layouts)
91
+ def layout name, lang: nil
92
+ find_entry(name, @parsed_layouts, @file_handler.layouts, lang: lang)
67
93
  end
68
94
 
69
- def include name
70
- find_entry(name, @parsed_includes, @file_handler.includes)
95
+ def include name, lang: nil
96
+ find_entry(name, @parsed_includes, @file_handler.includes, lang: lang)
71
97
  end
72
98
 
73
99
  def entry name
@@ -90,7 +116,7 @@ module Isola
90
116
 
91
117
  def render_to_dest entry
92
118
  if entry.instance_of? Source
93
- rendered, path = Context.new(entry, self).render
119
+ rendered, path = Context.new(entry, self, languages: languages).render
94
120
  dest_path = File.join(dest_dir, path)
95
121
  FileUtils.mkdir_p(File.dirname(dest_path))
96
122
  File.write(dest_path, rendered)
@@ -117,19 +143,36 @@ module Isola
117
143
  @parsed_entries = {}
118
144
  end
119
145
 
120
- def find_entry(name, cache, store)
121
- cache[name] ||=
146
+ def find_entry(name, cache, store, lang: nil)
147
+ resolved = resolve_localized(name, store, lang)
148
+ lang = @lang_router.language_for(resolved)
149
+ cache[resolved] ||=
122
150
  begin
123
- p = store[name]
151
+ p = store[resolved]
124
152
  return nil unless p
125
153
  if ext_to_process_with_tilt?(File.extname(p))
126
- Source.new(p, read_in_site(p))
154
+ translations = translations_for(resolved, store)
155
+ Source.new(p, read_in_site(p), lang: lang, translations: translations)
127
156
  else
128
157
  StaticFile.new(p)
129
158
  end
130
159
  end
131
160
  end
132
161
 
162
+ def resolve_localized(name, store, lang)
163
+ return name unless lang && @lang_router.language_for(name) != lang
164
+ localized = @lang_router.localized_path(name, lang)
165
+ store[localized] ? localized : name
166
+ end
167
+
168
+ def translations_for(path, store)
169
+ @lang_router.candidate_paths(path).select do |_, candidate|
170
+ store.key?(candidate)
171
+ end.transform_values do |candidate|
172
+ url_path_for(candidate)
173
+ end
174
+ end
175
+
133
176
  def read_in_site(p)
134
177
  File.read(File.join(config[:root_dir], p))
135
178
  end
data/lib/isola/source.rb CHANGED
@@ -3,13 +3,15 @@ require "yaml"
3
3
  module Isola
4
4
  class Source
5
5
  attr_reader :filepath, :meta, :content
6
- def initialize filepath, text
6
+ def initialize filepath, text, lang:, translations: {}
7
7
  @filepath = filepath
8
8
  @meta, @content = if (m = text.match(/\A---\s*\n(.+?)^---\s*\n(.*)\z/m))
9
- [YAML.safe_load(m[1], symbolize_names: true), m[2]]
9
+ [YAML.safe_load(m[1], symbolize_names: true) || {}, m[2]]
10
10
  else
11
11
  [{}, text]
12
12
  end
13
+ @meta[:lang] = lang
14
+ @meta[:translations] = translations
13
15
  end
14
16
 
15
17
  def [] key
data/lib/isola/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isola
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/isola.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "isola/version"
4
4
  require_relative "isola/site"
5
+ require_relative "isola/language_path_router"
5
6
  require_relative "isola/file_handler"
6
7
  require_relative "isola/source"
7
8
  require_relative "isola/static_file"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isola
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Satoshi Kojima
@@ -87,6 +87,7 @@ executables:
87
87
  extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
+ - ".rubocop.yml"
90
91
  - LICENSE
91
92
  - README.md
92
93
  - Rakefile
@@ -97,6 +98,7 @@ files:
97
98
  - lib/isola/context.rb
98
99
  - lib/isola/dev_server.rb
99
100
  - lib/isola/file_handler.rb
101
+ - lib/isola/language_path_router.rb
100
102
  - lib/isola/site.rb
101
103
  - lib/isola/source.rb
102
104
  - lib/isola/static_file.rb
@@ -121,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
123
  - !ruby/object:Gem::Version
122
124
  version: '0'
123
125
  requirements: []
124
- rubygems_version: 4.0.6
126
+ rubygems_version: 4.0.3
125
127
  specification_version: 4
126
128
  summary: very simple static site generator using ERB
127
129
  test_files: []