« get me outta code hell

content: encapsulate everything - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/content
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2024-06-14 22:10:12 -0300
committer(quasar) nebula <qznebula@protonmail.com>2024-06-18 22:56:12 -0300
commit99261a74b1f3298144d8c5c6d676d63ad27a1299 (patch)
tree953c14b248dbc1ead549d717369c6d6155d11f19 /src/content
parent53b65bd2cbfb1001194d67f57780e92c6c8c5aaf (diff)
content: encapsulate everything
Diffstat (limited to 'src/content')
-rw-r--r--src/content/dependencies/generateAlbumCommentaryPage.js125
-rw-r--r--src/content/dependencies/generateAlbumGalleryPage.js11
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js213
-rw-r--r--src/content/dependencies/generateAlbumNavAccent.js11
-rw-r--r--src/content/dependencies/generateAlbumReleaseInfo.js116
-rw-r--r--src/content/dependencies/generateAlbumSidebarGroupBox.js79
-rw-r--r--src/content/dependencies/generateAlbumSidebarTrackSection.js25
-rw-r--r--src/content/dependencies/generateAlbumSocialEmbed.js60
-rw-r--r--src/content/dependencies/generateAlbumTrackList.js41
-rw-r--r--src/content/dependencies/generateAlbumTrackListItem.js97
-rw-r--r--src/content/dependencies/generateAlbumTrackListMissingDuration.js34
-rw-r--r--src/content/dependencies/generateArtTagGalleryPage.js15
-rw-r--r--src/content/dependencies/generateArtistGalleryPage.js21
-rw-r--r--src/content/dependencies/generateArtistGroupContributionsInfo.js194
-rw-r--r--src/content/dependencies/generateArtistInfoPage.js341
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js23
-rw-r--r--src/content/dependencies/generateArtistInfoPageChunkItem.js68
-rw-r--r--src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js97
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunkItem.js19
-rw-r--r--src/content/dependencies/generateChronologyLinksScopeSwitcher.js21
-rw-r--r--src/content/dependencies/generateCommentaryEntry.js123
-rw-r--r--src/content/dependencies/generateCommentaryIndexPage.js86
-rw-r--r--src/content/dependencies/generateFlashActGalleryPage.js78
-rw-r--r--src/content/dependencies/generateFlashIndexPage.js143
-rw-r--r--src/content/dependencies/generateFlashInfoPage.js163
-rw-r--r--src/content/dependencies/generateGroupGalleryPage.js13
-rw-r--r--src/content/dependencies/generateGroupInfoPage.js71
-rw-r--r--src/content/dependencies/generateGroupInfoPageAlbumsSection.js128
-rw-r--r--src/content/dependencies/generateGroupSidebarCategoryDetails.js66
-rw-r--r--src/content/dependencies/generateNewsEntryPage.js74
-rw-r--r--src/content/dependencies/generateNewsIndexPage.js67
-rw-r--r--src/content/dependencies/generatePageLayout.js70
-rw-r--r--src/content/dependencies/generateSearchSidebarBox.js71
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js449
-rw-r--r--src/content/dependencies/generateTrackList.js21
-rw-r--r--src/content/dependencies/generateTrackListDividedByGroups.js104
-rw-r--r--src/content/dependencies/generateTrackReleaseInfo.js83
-rw-r--r--src/content/dependencies/generateTrackSocialEmbed.js52
-rw-r--r--src/content/dependencies/generateWikiHomeNewsBox.js81
-rw-r--r--src/content/dependencies/listRandomPageLinks.js56
-rw-r--r--src/content/dependencies/transformContent.js19
41 files changed, 1876 insertions, 1753 deletions
diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js
index 05dbdcf3..c14640af 100644
--- a/src/content/dependencies/generateAlbumCommentaryPage.js
+++ b/src/content/dependencies/generateAlbumCommentaryPage.js
@@ -130,11 +130,11 @@ export default {
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout
-      .slots({
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('albumCommentaryPage', pageCapsule =>
+      relations.layout.slots({
         title:
-          language.$('albumCommentaryPage.title', {
+          language.$(pageCapsule, 'title', {
             album: data.name,
           }),
 
@@ -146,7 +146,7 @@ export default {
         mainClasses: ['long-content'],
         mainContent: [
           html.tag('p',
-            language.$('albumCommentaryPage.infoLine', {
+            language.$(pageCapsule, 'infoLine', {
               words:
                 html.tag('b',
                   language.formatWordCount(data.wordCount, {unit: true})),
@@ -156,39 +156,41 @@ export default {
                   language.countCommentaryEntries(data.entryCount, {unit: true})),
             })),
 
-          relations.albumCommentaryEntries && [
-            relations.albumCommentaryHeading.slots({
-              tag: 'h3',
-              color: data.color,
-
-              title:
-                language.$('albumCommentaryPage.entry.title.albumCommentary', {
-                  album: relations.albumCommentaryLink,
-                }),
-
-              stickyTitle:
-                language.$('albumCommentaryPage.entry.title.albumCommentary.sticky', {
-                  album: data.name,
-                }),
-
-              accent:
-                language.$('albumCommentaryPage.entry.title.albumCommentary.accent', {
-                  [language.onlyIfOptions]: ['listeningLinks'],
-                  listeningLinks:
-                    language.formatUnitList(
-                      relations.albumCommentaryListeningLinks
-                        .map(link => link.slots({
-                          context: 'album',
-                          tab: 'separate',
-                        }))),
-                }),
-            }),
-
-            relations.albumCommentaryCover
-              ?.slots({mode: 'commentary'}),
-
-            relations.albumCommentaryEntries,
-          ],
+          relations.albumCommentaryEntries &&
+            language.encapsulate(pageCapsule, 'entry', entryCapsule => [
+              language.encapsulate(entryCapsule, 'title.albumCommentary', titleCapsule =>
+                relations.albumCommentaryHeading.slots({
+                  tag: 'h3',
+                  color: data.color,
+
+                  title:
+                    language.$(titleCapsule, {
+                      album: relations.albumCommentaryLink,
+                    }),
+
+                  stickyTitle:
+                    language.$(titleCapsule, 'sticky', {
+                      album: data.name,
+                    }),
+
+                  accent:
+                    language.$(titleCapsule, 'accent', {
+                      [language.onlyIfOptions]: ['listeningLinks'],
+                      listeningLinks:
+                        language.formatUnitList(
+                          relations.albumCommentaryListeningLinks
+                            .map(link => link.slots({
+                              context: 'album',
+                              tab: 'separate',
+                            }))),
+                    }),
+                })),
+
+              relations.albumCommentaryCover
+                ?.slots({mode: 'commentary'}),
+
+              relations.albumCommentaryEntries,
+            ]),
 
           stitchArrays({
             heading: relations.trackCommentaryHeadings,
@@ -206,31 +208,33 @@ export default {
               cover,
               entries,
               color,
-            }) => [
-              heading.slots({
-                tag: 'h3',
-                attributes: {id: directory},
-                color,
-
-                title:
-                  language.$('albumCommentaryPage.entry.title.trackCommentary', {
-                    track: link,
-                  }),
-
-                accent:
-                  language.$('albumCommentaryPage.entry.title.trackCommentary.accent', {
-                    [language.onlyIfOptions]: ['listeningLinks'],
-                    listeningLinks:
-                      language.formatUnitList(
-                        listeningLinks.map(link =>
-                          link.slot('tab', 'separate'))),
-                  }),
-              }),
+            }) =>
+              language.encapsulate(pageCapsule, 'entry', entryCapsule => [
+                language.encapsulate(entryCapsule, 'title.trackCommentary', titleCapsule =>
+                  heading.slots({
+                    tag: 'h3',
+                    attributes: {id: directory},
+                    color,
+
+                    title:
+                      language.$(titleCapsule, {
+                        track: link,
+                      }),
+
+                    accent:
+                      language.$(titleCapsule, 'accent', {
+                        [language.onlyIfOptions]: ['listeningLinks'],
+                        listeningLinks:
+                          language.formatUnitList(
+                            listeningLinks.map(link =>
+                              link.slot('tab', 'separate'))),
+                      }),
+                  })),
 
               cover?.slots({mode: 'commentary'}),
 
               entries.map(entry => entry.slot('color', color)),
-            ]),
+            ])),
         ],
 
         navLinkStyle: 'hierarchical',
@@ -251,6 +255,5 @@ export default {
         ],
 
         leftSidebar: relations.sidebar,
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js
index aa025688..44d49c54 100644
--- a/src/content/dependencies/generateAlbumGalleryPage.js
+++ b/src/content/dependencies/generateAlbumGalleryPage.js
@@ -160,11 +160,11 @@ export default {
     return data;
   },
 
-  generate(data, relations, {language}) {
-    return relations.layout
-      .slots({
+  generate: (data, relations, {language}) =>
+    language.encapsulate('albumGalleryPage', pageCapsule =>
+      relations.layout.slots({
         title:
-          language.$('albumGalleryPage.title', {
+          language.$(pageCapsule, 'title', {
             album: data.name,
           }),
 
@@ -223,6 +223,5 @@ export default {
         ],
 
         secondaryNav: relations.secondaryNav,
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index 9e4b8816..3af312bb 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -92,109 +92,120 @@ export default {
   }),
 
   generate: (data, relations, {html, language}) =>
-    relations.layout.slots({
-      title: language.$('albumPage.title', {album: data.name}),
-      headingMode: 'sticky',
-
-      color: data.color,
-      styleRules: [relations.albumStyleRules],
-
-      cover:
-        relations.cover
-          ?.slots({
-            alt: language.$('misc.alt.albumCover'),
-          })
-          ?? null,
-
-      mainContent: [
-        relations.releaseInfo,
-
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          {[html.joinChildren]: html.tag('br')},
-
-          [
-            !html.isBlank(relations.additionalFilesList) &&
-              language.$('releaseInfo.additionalFiles.shortcut', {
-                link: html.tag('a',
-                  {href: '#additional-files'},
-                  language.$('releaseInfo.additionalFiles.shortcut.link')),
+    language.encapsulate('albumPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            album: data.name,
+          }),
+
+        color: data.color,
+        headingMode: 'sticky',
+        styleRules: [relations.albumStyleRules],
+
+        cover:
+          relations.cover
+            ?.slots({
+              alt: language.$('misc.alt.albumCover'),
+            })
+            ?? null,
+
+        mainContent: [
+          relations.releaseInfo,
+
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+            {[html.joinChildren]: html.tag('br')},
+
+            language.encapsulate('releaseInfo', capsule => [
+              !html.isBlank(relations.additionalFilesList) &&
+                language.$(capsule, 'additionalFiles.shortcut', {
+                  link: html.tag('a',
+                    {href: '#additional-files'},
+                    language.$(capsule, 'additionalFiles.shortcut.link')),
+                }),
+
+              (relations.galleryLink && relations.commentaryLink
+                ? language.encapsulate(capsule, 'viewGalleryOrCommentary', capsule =>
+                    language.$(capsule, {
+                      gallery:
+                        relations.galleryLink
+                          .slot('content', language.$(capsule, 'gallery')),
+
+                      commentary:
+                        relations.commentaryLink
+                          .slot('content', language.$(capsule, 'commentary')),
+                    }))
+
+             : relations.galleryLink
+                ? language.encapsulate(capsule, 'viewGallery', capsule =>
+                    language.$(capsule, {
+                      link:
+                        relations.galleryLink
+                          .slot('content', language.$(capsule, 'link')),
+                    }))
+
+             : relations.commentaryLink
+                ? language.encapsulate(capsule, 'viewCommentary', capsule =>
+                    language.$(capsule, {
+                      link:
+                        relations.commentaryLink
+                          .slot('content', language.$(capsule, 'link')),
+                    }))
+
+                : html.blank()),
+            ])),
+
+          relations.trackList,
+
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+            {[html.joinChildren]: html.tag('br')},
+
+            language.encapsulate('releaseInfo', capsule => [
+              language.$(capsule, 'addedToWiki', {
+                [language.onlyIfOptions]: ['date'],
+                date: language.formatDate(data.dateAddedToWiki),
               }),
-
-            relations.galleryLink && relations.commentaryLink &&
-              language.$('releaseInfo.viewGalleryOrCommentary', {
-                gallery:
-                  relations.galleryLink
-                    .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.gallery')),
-                commentary:
-                  relations.commentaryLink
-                    .slot('content', language.$('releaseInfo.viewGalleryOrCommentary.commentary')),
+            ])),
+
+          language.encapsulate('releaseInfo.additionalFiles', capsule =>
+            html.tags([
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'additional-files'},
+                  title: language.$(capsule, 'heading'),
+                }),
+
+              relations.additionalFilesList,
+            ])),
+
+          relations.artistCommentarySection,
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {
+            auto: 'current',
+            accent:
+              relations.albumNavAccent.slots({
+                showTrackNavigation: true,
+                showExtraLinks: true,
               }),
+          },
+        ],
 
-            relations.galleryLink && !relations.commentaryLink &&
-              language.$('releaseInfo.viewGallery', {
-                link:
-                  relations.galleryLink
-                    .slot('content', language.$('releaseInfo.viewGallery.link')),
-              }),
+        navContent:
+          relations.chronologyLinks,
 
-            !relations.galleryLink && relations.commentaryLink &&
-              language.$('releaseInfo.viewCommentary', {
-                link:
-                  relations.commentaryLink
-                    .slot('content', language.$('releaseInfo.viewCommentary.link')),
-              }),
-          ]),
-
-        relations.trackList,
-
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          {[html.joinChildren]: html.tag('br')},
-
-          [
-            language.$('releaseInfo.addedToWiki', {
-              [language.onlyIfOptions]: ['date'],
-              date: language.formatDate(data.dateAddedToWiki),
-            }),
-          ]),
-
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'additional-files'},
-              title: language.$('releaseInfo.additionalFiles.heading'),
-            }),
-
-          relations.additionalFilesList,
-        ]),
-
-        relations.artistCommentarySection,
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {
-          auto: 'current',
-          accent:
-            relations.albumNavAccent.slots({
-              showTrackNavigation: true,
-              showExtraLinks: true,
-            }),
-        },
-      ],
-
-      navContent:
-        relations.chronologyLinks,
-
-      banner: relations.banner ?? null,
-      bannerPosition: 'top',
-
-      secondaryNav: relations.secondaryNav,
-
-      leftSidebar: relations.sidebar,
-
-      socialEmbed: relations.socialEmbed,
-    }),
+        banner: relations.banner ?? null,
+        bannerPosition: 'top',
+
+        secondaryNav: relations.secondaryNav,
+
+        leftSidebar: relations.sidebar,
+
+        socialEmbed: relations.socialEmbed,
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js
index 121af439..4b6fb062 100644
--- a/src/content/dependencies/generateAlbumNavAccent.js
+++ b/src/content/dependencies/generateAlbumNavAccent.js
@@ -62,18 +62,21 @@ export default {
   },
 
   generate(data, relations, slots, {html, language}) {
+    const albumNavCapsule = language.encapsulate('albumPage.nav');
+    const trackNavCapsule = language.encapsulate('trackPage.nav');
+
     const {content: extraLinks = []} =
       slots.showExtraLinks &&
         {content: [
           (!data.galleryIsStub || slots.currentExtra === 'gallery') &&
             relations.albumGalleryLink?.slots({
               attributes: {class: slots.currentExtra === 'gallery' && 'current'},
-              content: language.$('albumPage.nav.gallery'),
+              content: language.$(albumNavCapsule, 'gallery'),
             }),
 
           relations.albumCommentaryLink?.slots({
             attributes: {class: slots.currentExtra === 'commentary' && 'current'},
-            content: language.$('albumPage.nav.commentary'),
+            content: language.$(albumNavCapsule, 'commentary'),
           }),
         ]};
 
@@ -94,8 +97,8 @@ export default {
           {href: '#', 'data-random': 'track-in-sidebar'},
 
           (data.isTrackPage
-            ? language.$('trackPage.nav.random')
-            : language.$('albumPage.nav.randomTrack')));
+            ? language.$(trackNavCapsule, 'random')
+            : language.$(albumNavCapsule, 'randomTrack')));
 
     const allLinks = [
       ...previousNextLinks,
diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js
index 26e2e160..e4c184c0 100644
--- a/src/content/dependencies/generateAlbumReleaseInfo.js
+++ b/src/content/dependencies/generateAlbumReleaseInfo.js
@@ -59,61 +59,63 @@ export default {
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return html.tags([
-      html.tag('p',
-        {[html.onlyIfContent]: true},
-        {[html.joinChildren]: html.tag('br')},
-
-        [
-          relations.artistContributionsLine
-            .slots({stringKey: 'releaseInfo.by'}),
-
-          relations.coverArtistContributionsLine
-            .slots({stringKey: 'releaseInfo.coverArtBy'}),
-
-          relations.wallpaperArtistContributionsLine
-            .slots({stringKey: 'releaseInfo.wallpaperArtBy'}),
-
-          relations.bannerArtistContributionsLine
-            .slots({stringKey: 'releaseInfo.bannerArtBy'}),
-
-          language.$('releaseInfo.released', {
-            [language.onlyIfOptions]: ['date'],
-            date: language.formatDate(data.date),
-          }),
-
-          language.$('releaseInfo.artReleased', {
-            [language.onlyIfOptions]: ['date'],
-            date: language.formatDate(data.coverArtDate),
-          }),
-
-          language.$('releaseInfo.duration', {
-            [language.onlyIfOptions]: ['duration'],
-            duration:
-              language.formatDuration(data.duration, {
-                approximate: data.durationApproximate,
-              }),
-          }),
-        ]),
-
-      html.tag('p',
-        {[html.onlyIfContent]: true},
-        language.$('releaseInfo.listenOn', {
-          [language.onlyIfOptions]: ['links'],
-          links:
-            language.formatDisjunctionList(
-              relations.externalLinks
-                .map(link =>
-                  link.slot('context', [
-                    'album',
-                    (data.numTracks === 0
-                      ? 'albumNoTracks'
-                   : data.numTracks === 1
-                      ? 'albumOneTrack'
-                      : 'albumMultipleTracks'),
-                  ]))),
-        })),
-    ]);
-  },
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('releaseInfo', capsule =>
+      html.tags([
+        html.tag('p',
+          {[html.onlyIfContent]: true},
+          {[html.joinChildren]: html.tag('br')},
+
+          [
+            relations.artistContributionsLine
+              .slots({stringKey: capsule + '.by'}),
+
+            relations.coverArtistContributionsLine
+              .slots({stringKey: capsule + '.coverArtBy'}),
+
+            relations.wallpaperArtistContributionsLine
+              .slots({stringKey: capsule + '.wallpaperArtBy'}),
+
+            relations.bannerArtistContributionsLine
+              .slots({stringKey: capsule + '.bannerArtBy'}),
+
+            language.$(capsule, 'released', {
+              [language.onlyIfOptions]: ['date'],
+              date: language.formatDate(data.date),
+            }),
+
+            language.$(capsule, 'artReleased', {
+              [language.onlyIfOptions]: ['date'],
+              date: language.formatDate(data.coverArtDate),
+            }),
+
+            language.$(capsule, 'duration', {
+              [language.onlyIfOptions]: ['duration'],
+              duration:
+                language.formatDuration(data.duration, {
+                  approximate: data.durationApproximate,
+                }),
+            }),
+          ]),
+
+        html.tag('p',
+          {[html.onlyIfContent]: true},
+
+          language.$(capsule, 'listenOn', {
+            [language.onlyIfOptions]: ['links'],
+
+            links:
+              language.formatDisjunctionList(
+                relations.externalLinks
+                  .map(link =>
+                    link.slot('context', [
+                      'album',
+                      (data.numTracks === 0
+                        ? 'albumNoTracks'
+                     : data.numTracks === 1
+                        ? 'albumOneTrack'
+                        : 'albumMultipleTracks'),
+                    ]))),
+          })),
+      ])),
 };
diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js
index cc9b2c13..f3be74f7 100644
--- a/src/content/dependencies/generateAlbumSidebarGroupBox.js
+++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js
@@ -77,45 +77,50 @@ export default {
   },
 
   generate: (relations, slots, {html, language}) =>
-    relations.box.slots({
-      attributes: {class: 'individual-group-sidebar-box'},
-      content: [
-        html.tag('h1',
-          language.$('albumSidebar.groupBox.title', {
-            group: relations.groupLink,
-          })),
-
-        slots.mode === 'album' &&
-          relations.description
-            ?.slot('mode', 'multiline'),
-
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-
-          language.$('releaseInfo.visitOn', {
-            [language.onlyIfOptions]: ['links'],
-
-            links:
-              language.formatDisjunctionList(
-                relations.externalLinks
-                  .map(link => link.slot('context', 'group'))),
-          })),
-
-        slots.mode === 'album' &&
-          html.tag('p', {class: 'group-chronology-link'},
-            {[html.onlyIfContent]: true},
-            language.$('albumSidebar.groupBox.next', {
-              [language.onlyIfOptions]: ['album'],
-              album: relations.nextAlbumLink,
+    language.encapsulate('albumSidebar.groupBox', boxCapsule =>
+      relations.box.slots({
+        attributes: {class: 'individual-group-sidebar-box'},
+        content: [
+          html.tag('h1',
+            language.$(boxCapsule, 'title', {
+              group: relations.groupLink,
             })),
 
-        slots.mode === 'album' &&
-          html.tag('p', {class: 'group-chronology-link'},
+          slots.mode === 'album' &&
+            relations.description
+              ?.slot('mode', 'multiline'),
+
+          html.tag('p',
             {[html.onlyIfContent]: true},
-            language.$('albumSidebar.groupBox.previous', {
-              [language.onlyIfOptions]: ['album'],
-              album: relations.previousAlbumLink,
+
+            language.$('releaseInfo.visitOn', {
+              [language.onlyIfOptions]: ['links'],
+
+              links:
+                language.formatDisjunctionList(
+                  relations.externalLinks
+                    .map(link => link.slot('context', 'group'))),
             })),
-      ],
-    }),
+
+          slots.mode === 'album' &&
+            html.tag('p', {class: 'group-chronology-link'},
+              {[html.onlyIfContent]: true},
+
+              language.$(boxCapsule, 'next', {
+                [language.onlyIfOptions]: ['album'],
+
+                album: relations.nextAlbumLink,
+              })),
+
+          slots.mode === 'album' &&
+            html.tag('p', {class: 'group-chronology-link'},
+              {[html.onlyIfContent]: true},
+
+              language.$(boxCapsule, 'previous', {
+                [language.onlyIfOptions]: ['album'],
+
+                album: relations.previousAlbumLink,
+              })),
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js
index aa5c723d..d59218b8 100644
--- a/src/content/dependencies/generateAlbumSidebarTrackSection.js
+++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js
@@ -55,10 +55,12 @@ export default {
   },
 
   generate(data, relations, slots, {getColors, html, language}) {
+    const capsule = language.encapsulate('albumSidebar.trackList');
+
     const sectionName =
       html.tag('span', {class: 'group-name'},
         (data.isDefaultTrackSection
-          ? language.$('albumSidebar.trackList.fallbackSectionName')
+          ? language.$(capsule, 'fallbackSectionName')
           : data.name));
 
     let colorStyle;
@@ -78,7 +80,7 @@ export default {
           data.tracksAreMissingCommentary[index] &&
             {class: 'no-commentary'},
 
-          language.$('albumSidebar.trackList.item', {
+          language.$(capsule, 'item', {
             track:
               (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index]
                 ? trackLink.slots({
@@ -117,14 +119,17 @@ export default {
           colorStyle,
 
           html.tag('span',
-            (data.hasTrackNumbers
-              ? language.$('albumSidebar.trackList.group.withRange', {
-                  group: sectionName,
-                  range: `${data.firstTrackNumber}–${data.lastTrackNumber}`
-                })
-              : language.$('albumSidebar.trackList.group', {
-                  group: sectionName,
-                })))),
+            language.encapsulate(capsule, 'group', capsule => {
+              const options = {group: sectionName};
+
+              if (data.hasTrackNumbers) {
+                capsule += '.withRange';
+                options.range =
+                  `${data.firstTrackNumber}–${data.lastTrackNumber}`;
+              }
+
+              return language.$(capsule, options);
+            }))),
 
         (data.hasTrackNumbers
           ? html.tag('ol',
diff --git a/src/content/dependencies/generateAlbumSocialEmbed.js b/src/content/dependencies/generateAlbumSocialEmbed.js
index c8b123fe..7500109e 100644
--- a/src/content/dependencies/generateAlbumSocialEmbed.js
+++ b/src/content/dependencies/generateAlbumSocialEmbed.js
@@ -41,34 +41,34 @@ export default {
     return data;
   },
 
-  generate(data, relations, {absoluteTo, language, urls}) {
-    return relations.socialEmbed.slots({
-      title:
-        language.$('albumPage.socialEmbed.title', {
-          album: data.albumName,
-        }),
-
-      description: relations.description,
-
-      headingContent:
-        (data.hasHeading
-          ? language.$('albumPage.socialEmbed.heading', {
-              group: data.headingGroupName,
-            })
-          : null),
-
-      headingLink:
-        (data.hasHeading
-          ? absoluteTo('localized.groupGallery', data.headingGroupDirectory)
-          : null),
-
-      imagePath:
-        (data.hasImage
-          ? '/' +
-            urls
-              .from('shared.root')
-              .to('media.albumCover', data.coverArtDirectory, data.coverArtFileExtension)
-          : null),
-    });
-  },
+  generate: (data, relations, {absoluteTo, language, urls}) =>
+    language.encapsulate('albumPage.socialEmbed', embedCapsule =>
+      relations.socialEmbed.slots({
+        title:
+          language.$(embedCapsule, 'title', {
+            album: data.albumName,
+          }),
+
+        description: relations.description,
+
+        headingContent:
+          (data.hasHeading
+            ? language.$(embedCapsule, 'heading', {
+                group: data.headingGroupName,
+              })
+            : null),
+
+        headingLink:
+          (data.hasHeading
+            ? absoluteTo('localized.groupGallery', data.headingGroupDirectory)
+            : null),
+
+        imagePath:
+          (data.hasImage
+            ? '/' +
+              urls
+                .from('shared.root')
+                .to('media.albumCover', data.coverArtDirectory, data.coverArtFileExtension)
+            : null),
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumTrackList.js b/src/content/dependencies/generateAlbumTrackList.js
index dd3e85e3..a3435bea 100644
--- a/src/content/dependencies/generateAlbumTrackList.js
+++ b/src/content/dependencies/generateAlbumTrackList.js
@@ -147,27 +147,30 @@ export default {
               durationApproximate,
               startIndex,
             }) => [
-              heading.slots({
-                tag: 'dt',
-
-                title:
-                  (duration === 0
-                    ? language.$('trackList.section', {
-                        section: name,
-                      })
-                    : language.$('trackList.section.withDuration', {
-                        section: name,
-                        duration:
+              language.encapsulate('trackList.section', capsule =>
+                heading.slots({
+                  tag: 'dt',
+
+                  title:
+                    language.encapsulate(capsule, capsule => {
+                      const options = {section: name};
+
+                      if (duration !== 0) {
+                        capsule += '.withDuration';
+                        options.duration =
                           language.formatDuration(duration, {
                             approximate: durationApproximate,
-                          }),
-                      })),
-
-                stickyTitle:
-                  language.$('trackList.section.sticky', {
-                    section: name,
-                  }),
-              }),
+                          });
+                      }
+
+                      return language.$(capsule, options);
+                    }),
+
+                  stickyTitle:
+                    language.$(capsule, 'sticky', {
+                      section: name,
+                    }),
+                })),
 
               html.tag('dd',
                 html.tag(listTag,
diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js
index 7190fb4c..7d5d2c6e 100644
--- a/src/content/dependencies/generateAlbumTrackListItem.js
+++ b/src/content/dependencies/generateAlbumTrackListItem.js
@@ -80,54 +80,51 @@ export default {
     },
   },
 
-  generate(data, relations, slots, {getColors, html, language}) {
-    let colorStyle;
-    if (data.color) {
-      const {primary} = getColors(data.color);
-      colorStyle = {style: `--primary-color: ${primary}`};
-    }
-
-    const parts = ['trackList.item'];
-    const options = {};
-
-    options.track =
-      relations.trackLink
-        .slot('color', false);
-
-    const collapseDuration =
-      (slots.collapseDurationScope === 'track'
-        ? !data.trackHasDuration
-     : slots.collapseDurationScope === 'section'
-        ? !data.sectionHasDuration
-     : slots.collapseDurationScope === 'album'
-        ? !data.albumHasDuration
-        : false);
-
-    if (!collapseDuration) {
-      parts.push('withDuration');
-
-      options.duration =
-        (data.trackHasDuration
-          ? language.$('trackList.item.withDuration.duration', {
-              duration:
-                language.formatDuration(data.duration),
-            })
-          : relations.missingDuration);
-    }
-
-    if (data.showArtists) {
-      parts.push('withArtists');
-      options.by =
-        html.tag('span', {class: 'by'},
-          html.metatag('chunkwrap', {split: ','},
-            html.resolve(
-              language.$('trackList.item.withArtists.by', {
-                artists: language.formatConjunctionList(relations.contributionLinks),
-              }))));
-    }
-
-    return html.tag('li',
-      colorStyle,
-      language.formatString(...parts, options));
-  },
+  generate: (data, relations, slots, {getColors, html, language}) =>
+    language.encapsulate('trackList.item', itemCapsule =>
+      html.tag('li',
+        data.color &&
+          {style: `--primary-color: ${getColors(data.color).primary}`},
+
+        language.encapsulate(itemCapsule, workingCapsule => {
+          const workingOptions = {};
+
+          workingOptions.track =
+            relations.trackLink
+              .slot('color', false);
+
+          const collapseDuration =
+            (slots.collapseDurationScope === 'track'
+              ? !data.trackHasDuration
+           : slots.collapseDurationScope === 'section'
+              ? !data.sectionHasDuration
+           : slots.collapseDurationScope === 'album'
+              ? !data.albumHasDuration
+              : false);
+
+          if (!collapseDuration) {
+            workingCapsule += '.withDuration';
+            workingOptions.duration =
+              (data.trackHasDuration
+                ? language.$(itemCapsule, 'withDuration.duration', {
+                    duration:
+                      language.formatDuration(data.duration),
+                  })
+                : relations.missingDuration);
+          }
+
+          if (data.showArtists) {
+            workingCapsule += '.withArtists';
+            workingOptions.by =
+              html.tag('span', {class: 'by'},
+                html.metatag('chunkwrap', {split: ','},
+                  html.resolve(
+                    language.$(itemCapsule, 'withArtists.by', {
+                      artists:
+                        language.formatConjunctionList(relations.contributionLinks),
+                    }))));
+          }
+
+          return language.$(workingCapsule, workingOptions);
+        }))),
 };
diff --git a/src/content/dependencies/generateAlbumTrackListMissingDuration.js b/src/content/dependencies/generateAlbumTrackListMissingDuration.js
index 6d4a6ec8..b5917982 100644
--- a/src/content/dependencies/generateAlbumTrackListMissingDuration.js
+++ b/src/content/dependencies/generateAlbumTrackListMissingDuration.js
@@ -11,23 +11,25 @@ export default {
   }),
 
   generate: (relations, {html, language}) =>
-    relations.textWithTooltip.slots({
-      attributes: {class: 'missing-duration'},
-      customInteractionCue: true,
+    language.encapsulate('trackList.item.withDuration', itemCapsule =>
+      language.encapsulate(itemCapsule, 'duration', durationCapsule =>
+        relations.textWithTooltip.slots({
+          attributes: {class: 'missing-duration'},
+          customInteractionCue: true,
 
-      text:
-        language.$('trackList.item.withDuration.duration', {
-          duration:
-            html.tag('span', {class: 'text-with-tooltip-interaction-cue'},
-              language.$('trackList.item.withDuration.duration.missing')),
-        }),
+          text:
+            language.$(durationCapsule, {
+              duration:
+                html.tag('span', {class: 'text-with-tooltip-interaction-cue'},
+                  language.$(durationCapsule, 'missing')),
+            }),
 
-      tooltip:
-        relations.tooltip.slots({
-          attributes: {class: 'missing-duration-tooltip'},
+          tooltip:
+            relations.tooltip.slots({
+              attributes: {class: 'missing-duration-tooltip'},
 
-          content:
-            language.$('trackList.item.withDuration.duration.missing.info'),
-        }),
-    }),
+              content:
+                language.$(durationCapsule, 'missing.info'),
+            }),
+        }))),
 };
diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js
index eae48f05..c51faeba 100644
--- a/src/content/dependencies/generateArtTagGalleryPage.js
+++ b/src/content/dependencies/generateArtTagGalleryPage.js
@@ -85,11 +85,11 @@ export default {
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout
-      .slots({
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('tagPage', pageCapsule =>
+      relations.layout.slots({
         title:
-          language.$('tagPage.title', {
+          language.$(pageCapsule, 'title', {
             tag: data.name,
           }),
 
@@ -100,7 +100,7 @@ export default {
         mainClasses: ['top-index'],
         mainContent: [
           html.tag('p', {class: 'quick-info'},
-            language.$('tagPage.infoLine', {
+            language.$(pageCapsule, 'infoLine', {
               coverArts: language.countCoverArts(data.numArtworks, {
                 unit: true,
               }),
@@ -143,11 +143,10 @@ export default {
 
           {
             html:
-              language.$('tagPage.nav.tag', {
+              language.$(pageCapsule, 'nav.tag', {
                 tag: relations.artTagMainLink,
               }),
           },
         ],
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js
index 26a894c6..28f06a21 100644
--- a/src/content/dependencies/generateArtistGalleryPage.js
+++ b/src/content/dependencies/generateArtistGalleryPage.js
@@ -84,11 +84,11 @@ export default {
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout
-      .slots({
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('artistGalleryPage', pageCapsule =>
+      relations.layout.slots({
         title:
-          language.$('artistGalleryPage.title', {
+          language.$(pageCapsule, 'title', {
             artist: data.name,
           }),
 
@@ -97,10 +97,11 @@ export default {
         mainClasses: ['top-index'],
         mainContent: [
           html.tag('p', {class: 'quick-info'},
-            language.$('artistGalleryPage.infoLine', {
-              coverArts: language.countCoverArts(data.numArtworks, {
-                unit: true,
-              }),
+            language.$(pageCapsule, 'infoLine', {
+              coverArts:
+                language.countCoverArts(data.numArtworks, {
+                  unit: true,
+                }),
             })),
 
           relations.coverGrid
@@ -119,6 +120,7 @@ export default {
                       dimensions,
                     })),
 
+              // TODO: Can this be [language.onlyIfOptions]?
               info:
                 data.otherCoverArtists.map(names =>
                   (names === null
@@ -137,6 +139,5 @@ export default {
               currentExtra: 'gallery',
             })
             .content,
-      })
-  },
+      })),
 }
diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js
index ef81739d..f84d00de 100644
--- a/src/content/dependencies/generateArtistGroupContributionsInfo.js
+++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js
@@ -131,100 +131,104 @@ export default {
     countUnit: {validate: v => v.is('tracks', 'artworks')},
   },
 
-  generate(data, relations, slots, {html, language}) {
-    if (slots.sort === 'count' && empty(relations.groupLinksSortedByCount)) {
-      return html.blank();
-    } else if (slots.sort === 'duration' && empty(relations.groupLinksSortedByDuration)) {
-      return html.blank();
-    }
+  generate: (data, relations, slots, {html, language}) =>
+    language.encapsulate('artistPage.groupContributions', capsule => {
+      if (slots.sort === 'count' && empty(relations.groupLinksSortedByCount)) {
+        return html.blank();
+      } else if (slots.sort === 'duration' && empty(relations.groupLinksSortedByDuration)) {
+        return html.blank();
+      }
 
-    const getCounts = counts =>
-      counts.map(count => {
-        switch (slots.countUnit) {
-          case 'tracks': return language.countTracks(count, {unit: true});
-          case 'artworks': return language.countArtworks(count, {unit: true});
-        }
-      });
-
-    // We aren't displaying the "~" approximate symbol here for now.
-    // The general notion that these sums aren't going to be 100% accurate
-    // is made clear by the "XYZ has contributed ~1:23:45 hours of music..."
-    // line that's always displayed above this table.
-    const getDurations = (durations, approximate) =>
-      stitchArrays({
-        duration: durations,
-        approximate: approximate,
-      }).map(({duration}) => language.formatDuration(duration));
-
-    const topLevelClasses = [
-      'group-contributions-sorted-by-' + slots.sort,
-      slots.visible && 'visible',
-    ];
-
-    // TODO: It feels pretty awkward that this component is the only one that
-    // has enough knowledge to decide if the sort button is even applicable...
-    const switchingSortPossible =
-      !empty(relations.groupLinksSortedByCount) &&
-      !empty(relations.groupLinksSortedByDuration);
-
-    return html.tags([
-      html.tag('dt', {class: topLevelClasses},
-        (switchingSortPossible && slots.showSortButton
-          ? language.$('artistPage.groupContributions.title.withSortButton', {
-              title: slots.title,
-              sort:
-                html.tag('a', {class: 'group-contributions-sort-button'},
-                  {href: '#'},
-
-                  (slots.sort === 'count'
-                    ? language.$('artistPage.groupContributions.title.sorting.count')
-                    : language.$('artistPage.groupContributions.title.sorting.duration'))),
-            })
-          : slots.title)),
-
-      html.tag('dd', {class: topLevelClasses},
-        html.tag('ul', {class: 'group-contributions-table'},
-          {role: 'list'},
-
-          (slots.sort === 'count'
-            ? stitchArrays({
-                group: relations.groupLinksSortedByCount,
-                count: getCounts(data.groupCountsSortedByCount),
-                duration:
-                  getDurations(
-                    data.groupDurationsSortedByCount,
-                    data.groupDurationsApproximateSortedByCount),
-              }).map(({group, count, duration}) =>
-                  html.tag('li',
-                    html.tag('div', {class: 'group-contributions-row'}, [
-                      group,
-                      html.tag('span', {class: 'group-contributions-metrics'},
-                        // When sorting by count, duration details aren't necessarily
-                        // available for all items.
-                        (slots.showBothColumns && duration
-                          ? language.$('artistPage.groupContributions.item.countDurationAccent', {count, duration})
-                          : language.$('artistPage.groupContributions.item.countAccent', {count}))),
-                    ])))
-
-            : stitchArrays({
-                group: relations.groupLinksSortedByDuration,
-                count: getCounts(data.groupCountsSortedByDuration),
-                duration:
-                  getDurations(
-                    data.groupDurationsSortedByDuration,
-                    data.groupDurationsApproximateSortedByDuration),
-              }).map(({group, count, duration}) =>
-                  html.tag('li',
-                    html.tag('div', {class: 'group-contributions-row'}, [
-                      group,
-                      html.tag('span', {class: 'group-contributions-metrics'},
-                        // Count details are always available, since they're just the
-                        // number of contributions directly. And duration details are
-                        // guaranteed for every item when sorting by duration.
-                        (slots.showBothColumns
-                          ? language.$('artistPage.groupContributions.item.durationCountAccent', {duration, count})
-                          : language.$('artistPage.groupContributions.item.durationAccent', {duration}))),
-                    ])))))),
-    ]);
-  },
+      const getCounts = counts =>
+        counts.map(count => {
+          switch (slots.countUnit) {
+            case 'tracks': return language.countTracks(count, {unit: true});
+            case 'artworks': return language.countArtworks(count, {unit: true});
+          }
+        });
+
+      // We aren't displaying the "~" approximate symbol here for now.
+      // The general notion that these sums aren't going to be 100% accurate
+      // is made clear by the "XYZ has contributed ~1:23:45 hours of music..."
+      // line that's always displayed above this table.
+      const getDurations = (durations, approximate) =>
+        stitchArrays({
+          duration: durations,
+          approximate: approximate,
+        }).map(({duration}) => language.formatDuration(duration));
+
+      const topLevelClasses = [
+        'group-contributions-sorted-by-' + slots.sort,
+        slots.visible && 'visible',
+      ];
+
+      // TODO: It feels pretty awkward that this component is the only one that
+      // has enough knowledge to decide if the sort button is even applicable...
+      const switchingSortPossible =
+        !empty(relations.groupLinksSortedByCount) &&
+        !empty(relations.groupLinksSortedByDuration);
+
+      return html.tags([
+        html.tag('dt', {class: topLevelClasses},
+          language.encapsulate(capsule, 'title', capsule =>
+            (switchingSortPossible && slots.showSortButton
+              ? language.$(capsule, 'withSortButton', {
+                  title: slots.title,
+                  sort:
+                    html.tag('a', {class: 'group-contributions-sort-button'},
+                      {href: '#'},
+
+                      (slots.sort === 'count'
+                        ? language.$(capsule, 'sorting.count')
+                        : language.$(capsule, 'sorting.duration'))),
+                })
+              : slots.title))),
+
+        html.tag('dd', {class: topLevelClasses},
+          html.tag('ul', {class: 'group-contributions-table'},
+            {role: 'list'},
+
+            (slots.sort === 'count'
+              ? stitchArrays({
+                  group: relations.groupLinksSortedByCount,
+                  count: getCounts(data.groupCountsSortedByCount),
+                  duration:
+                    getDurations(
+                      data.groupDurationsSortedByCount,
+                      data.groupDurationsApproximateSortedByCount),
+                }).map(({group, count, duration}) =>
+                    language.encapsulate(capsule, 'item', capsule =>
+                      html.tag('li',
+                        html.tag('div', {class: 'group-contributions-row'}, [
+                          group,
+                          html.tag('span', {class: 'group-contributions-metrics'},
+                            // When sorting by count, duration details aren't necessarily
+                            // available for all items.
+                            (slots.showBothColumns && duration
+                              ? language.$(capsule, 'countDurationAccent', {count, duration})
+                              : language.$(capsule, 'countAccent', {count}))),
+                        ]))))
+
+              : stitchArrays({
+                  group: relations.groupLinksSortedByDuration,
+                  count: getCounts(data.groupCountsSortedByDuration),
+                  duration:
+                    getDurations(
+                      data.groupDurationsSortedByDuration,
+                      data.groupDurationsApproximateSortedByDuration),
+                }).map(({group, count, duration}) =>
+                    language.encapsulate(capsule, 'item', capsule =>
+                      html.tag('li',
+                        html.tag('div', {class: 'group-contributions-row'}, [
+                          group,
+                          html.tag('span', {class: 'group-contributions-metrics'},
+                            // Count details are always available, since they're just the
+                            // number of contributions directly. And duration details are
+                            // guaranteed for every item when sorting by duration.
+                            (slots.showBothColumns
+                              ? language.$(capsule, 'durationCountAccent', {duration, count})
+                              : language.$(capsule, 'durationAccent', {duration}))),
+                        ]))))))),
+      ]);
+    }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js
index dd56b757..f9ce7e3b 100644
--- a/src/content/dependencies/generateArtistInfoPage.js
+++ b/src/content/dependencies/generateArtistInfoPage.js
@@ -115,185 +115,198 @@ export default {
   }),
 
   generate: (data, relations, {html, language}) =>
-    relations.layout.slots({
-      title: data.name,
-      headingMode: 'sticky',
-
-      cover:
-        (relations.cover
-          ? relations.cover.slots({
-              path: [
-                'media.artistAvatar',
-                data.directory,
-                data.avatarFileExtension,
-              ],
-            })
-          : null),
+    language.encapsulate('artistPage', pageCapsule =>
+      relations.layout.slots({
+        title: data.name,
+        headingMode: 'sticky',
+
+        cover:
+          (relations.cover
+            ? relations.cover.slots({
+                path: [
+                  'media.artistAvatar',
+                  data.directory,
+                  data.avatarFileExtension,
+                ],
+              })
+            : null),
+
+        mainContent: [
+          html.tags([
+            html.tag('p',
+              {[html.onlyIfSiblings]: true},
+              language.$('releaseInfo.note')),
+
+            html.tag('blockquote',
+              {[html.onlyIfContent]: true},
+              relations.contextNotes),
+          ]),
 
-      mainContent: [
-        html.tags([
           html.tag('p',
-            {[html.onlyIfSiblings]: true},
-            language.$('releaseInfo.note')),
+            {[html.onlyIfContent]: true},
 
-          html.tag('blockquote',
+            language.$('releaseInfo.visitOn', {
+              [language.onlyIfOptions]: ['links'],
+
+              links:
+                language.formatDisjunctionList(
+                  relations.visitLinks
+                    .map(link => link.slot('context', 'artist'))),
+            })),
+
+          html.tag('p',
             {[html.onlyIfContent]: true},
-            relations.contextNotes),
-        ]),
-
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          language.$('releaseInfo.visitOn', {
-            [language.onlyIfOptions]: ['links'],
-            links:
-              language.formatDisjunctionList(
-                relations.visitLinks
-                  .map(link => link.slot('context', 'artist'))),
-          })),
-
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          language.$('artistPage.viewArtGallery', {
-            [language.onlyIfOptions]: ['link'],
-            link:
-              relations.artistGalleryLink?.slots({
-                content: language.$('artistPage.viewArtGallery.link'),
-              }),
-          })),
-
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          language.$('misc.jumpTo.withLinks', {
-            [language.onlyIfOptions]: ['links'],
-            links:
-              language.formatUnitList([
-                !html.isBlank(relations.tracksChunkedList) &&
-                  html.tag('a',
-                    {href: '#tracks'},
-                    language.$('artistPage.trackList.title')),
-
-                !html.isBlank(relations.artworksChunkedList) &&
-                  html.tag('a',
-                    {href: '#art'},
-                    language.$('artistPage.artList.title')),
-
-                !html.isBlank(relations.flashesChunkedList) &&
-                  html.tag('a',
-                    {href: '#flashes'},
-                    language.$('artistPage.flashList.title')),
-
-                !html.isBlank(relations.commentaryChunkedList) &&
-                  html.tag('a',
-                    {href: '#commentary'},
-                    language.$('artistPage.commentaryList.title')),
-              ].filter(Boolean)),
-          })),
-
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              tag: 'h2',
-              attributes: {id: 'tracks'},
-              title: language.$('artistPage.trackList.title'),
-            }),
 
-          data.totalDuration > 0 &&
-            html.tag('p',
-              {[html.onlyIfSiblings]: true},
-              language.$('artistPage.contributedDurationLine', {
-                artist: data.name,
-                duration:
-                  language.formatDuration(data.totalDuration, {
-                    approximate: data.totalTrackCount > 1,
-                    unit: true,
+            language.encapsulate(pageCapsule, 'viewArtGallery', capsule =>
+              language.$(capsule, {
+                [language.onlyIfOptions]: ['link'],
+
+                link:
+                  relations.artistGalleryLink?.slots({
+                    content:
+                      language.$(capsule, 'link'),
                   }),
-              })),
-
-          relations.tracksChunkedList.slots({
-            groupInfo: [
-              relations.tracksGroupInfo
-                .clone()
-                .slots({
-                  title: language.$('artistPage.groupContributions.title.music'),
-                  showSortButton: true,
-                  sort: 'count',
-                  countUnit: 'tracks',
-                  visible: true,
-                }),
-
-              relations.tracksGroupInfo
-                .clone()
-                .slots({
-                  title: language.$('artistPage.groupContributions.title.music'),
-                  showSortButton: true,
-                  sort: 'duration',
-                  countUnit: 'tracks',
-                  visible: false,
-                }),
-            ],
-          }),
-        ]),
-
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              tag: 'h2',
-              attributes: {id: 'art'},
-              title: language.$('artistPage.artList.title'),
-            }),
+              }))),
 
           html.tag('p',
             {[html.onlyIfContent]: true},
-            language.$('artistPage.viewArtGallery.orBrowseList', {
-              [language.onlyIfOptions]: ['link'],
-              link:
-                relations.artistGalleryLink?.slots({
-                  content: language.$('artistPage.viewArtGallery.link'),
-                }),
+
+            language.$('misc.jumpTo.withLinks', {
+              [language.onlyIfOptions]: ['links'],
+
+              links:
+                language.formatUnitList([
+                  !html.isBlank(relations.tracksChunkedList) &&
+                    html.tag('a',
+                      {href: '#tracks'},
+                      language.$(pageCapsule, 'trackList.title')),
+
+                  !html.isBlank(relations.artworksChunkedList) &&
+                    html.tag('a',
+                      {href: '#art'},
+                      language.$(pageCapsule, 'artList.title')),
+
+                  !html.isBlank(relations.flashesChunkedList) &&
+                    html.tag('a',
+                      {href: '#flashes'},
+                      language.$(pageCapsule, 'flashList.title')),
+
+                  !html.isBlank(relations.commentaryChunkedList) &&
+                    html.tag('a',
+                      {href: '#commentary'},
+                      language.$(pageCapsule, 'commentaryList.title')),
+                ].filter(Boolean)),
             })),
 
-          relations.artworksChunkedList
-            .slots({
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                tag: 'h2',
+                attributes: {id: 'tracks'},
+                title: language.$(pageCapsule, 'trackList.title'),
+              }),
+
+            data.totalDuration > 0 &&
+              html.tag('p',
+                {[html.onlyIfSiblings]: true},
+
+                language.$(pageCapsule, 'contributedDurationLine', {
+                  artist: data.name,
+                  duration:
+                    language.formatDuration(data.totalDuration, {
+                      approximate: data.totalTrackCount > 1,
+                      unit: true,
+                    }),
+                })),
+
+            relations.tracksChunkedList.slots({
               groupInfo:
-                relations.artworksGroupInfo
-                  .slots({
-                    title: language.$('artistPage.groupContributions.title.artworks'),
-                    showBothColumns: false,
-                    sort: 'count',
-                    countUnit: 'artworks',
-                  }),
+                language.encapsulate(pageCapsule, 'groupContributions', capsule => [
+                  relations.tracksGroupInfo.clone()
+                    .slots({
+                      title: language.$(capsule, 'title.music'),
+                      showSortButton: true,
+                      sort: 'count',
+                      countUnit: 'tracks',
+                      visible: true,
+                    }),
+
+                  relations.tracksGroupInfo.clone()
+                    .slots({
+                      title: language.$(capsule, 'title.music'),
+                      showSortButton: true,
+                      sort: 'duration',
+                      countUnit: 'tracks',
+                      visible: false,
+                    }),
+                ]),
             }),
-        ]),
+          ]),
+
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                tag: 'h2',
+                attributes: {id: 'art'},
+                title: language.$(pageCapsule, 'artList.title'),
+              }),
 
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              tag: 'h2',
-              attributes: {id: 'flashes'},
-              title: language.$('artistPage.flashList.title'),
-            }),
+            html.tag('p',
+              {[html.onlyIfContent]: true},
+
+              language.encapsulate(pageCapsule, 'viewArtGallery', capsule =>
+                language.$(capsule, 'orBrowseList', {
+                  [language.onlyIfOptions]: ['link'],
+
+                  link:
+                    relations.artistGalleryLink?.slots({
+                      content: language.$(capsule, 'link'),
+                    }),
+                }))),
+
+            relations.artworksChunkedList
+              .slots({
+                groupInfo:
+                  language.encapsulate(pageCapsule, 'groupContributions', capsule =>
+                    relations.artworksGroupInfo
+                      .slots({
+                        title: language.$(capsule, 'title.artworks'),
+                        showBothColumns: false,
+                        sort: 'count',
+                        countUnit: 'artworks',
+                      })),
+              }),
+          ]),
+
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                tag: 'h2',
+                attributes: {id: 'flashes'},
+                title: language.$(pageCapsule, 'flashList.title'),
+              }),
 
-          relations.flashesChunkedList,
-        ]),
+            relations.flashesChunkedList,
+          ]),
 
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              tag: 'h2',
-              attributes: {id: 'commentary'},
-              title: language.$('artistPage.commentaryList.title'),
-            }),
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                tag: 'h2',
+                attributes: {id: 'commentary'},
+                title: language.$(pageCapsule, 'commentaryList.title'),
+              }),
+
+            relations.commentaryChunkedList,
+          ]),
+        ],
 
-          relations.commentaryChunkedList,
-        ]),
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks:
-        relations.artistNavLinks
-          .slots({
-            showExtraLinks: true,
-          })
-          .content,
-    }),
+        navLinkStyle: 'hierarchical',
+        navLinks:
+          relations.artistNavLinks
+            .slots({
+              showExtraLinks: true,
+            })
+            .content,
+      })),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
index 098b9e8f..e8d887b1 100644
--- a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
@@ -46,16 +46,17 @@ export default {
       annotation: data.annotation,
 
       content:
-        (data.kind === 'track-cover'
-          ? language.$('artistPage.creditList.entry.track', {
-              track: relations.trackLink,
-            })
-          : html.tag('i',
-              language.$('artistPage.creditList.entry.album',
-                {
-                  'wallpaper': 'wallpaperArt',
-                  'banner': 'bannerArt',
-                  'album-cover': 'coverArt',
-                }[data.kind]))),
+        language.encapsulate('artistPage.creditList.entry', capsule =>
+          (data.kind === 'track-cover'
+            ? language.$(capsule, 'track', {
+                track: relations.trackLink,
+              })
+            : html.tag('i',
+                language.encapsulate(capsule, 'album', capsule =>
+                  (data.kind === 'wallpaper'
+                    ? language.$(capsule, 'wallpaperArt')
+                 : data.kind === 'banner'
+                    ? language.$(capsule, 'bannerArt')
+                    : language.$(capsule, 'coverArt')))))),
     }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js
index ee172f48..9d406c67 100644
--- a/src/content/dependencies/generateArtistInfoPageChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js
@@ -21,42 +21,38 @@ export default {
     rerelease: {type: 'boolean'},
   },
 
-  generate(slots, {html, language}) {
-    let accentedContent = slots.content;
-
-    accent: {
-      if (slots.rerelease) {
-        accentedContent =
-          language.$('artistPage.creditList.entry.rerelease', {
-            entry: accentedContent,
-          });
-
-        break accent;
-      }
-
-      const parts = ['artistPage.creditList.entry'];
-      const options = {entry: accentedContent};
-
-      if (!empty(slots.otherArtistLinks)) {
-        parts.push('withArtists');
-        options.artists = language.formatConjunctionList(slots.otherArtistLinks);
-      }
-
-      if (!html.isBlank(slots.annotation)) {
-        parts.push('withAnnotation');
-        options.annotation = slots.annotation;
-      }
-
-      if (parts.length === 1) {
-        break accent;
-      }
-
-      accentedContent = language.formatString(...parts, options);
-    }
-
-    return (
+  generate: (slots, {html, language}) =>
+    language.encapsulate('artistPage.creditList.entry', entryCapsule =>
       html.tag('li',
         slots.rerelease && {class: 'rerelease'},
-        accentedContent));
-  },
+
+        language.encapsulate(entryCapsule, workingCapsule => {
+          const workingOptions = {entry: slots.content};
+
+          if (slots.rerelease) {
+            workingCapsule += '.rerelease';
+            return language.$(workingCapsule, workingOptions);
+          }
+
+          let anyAccent = false;
+
+          if (!empty(slots.otherArtistLinks)) {
+            anyAccent = true;
+            workingCapsule += '.withArtists';
+            workingOptions.artists =
+              language.formatConjunctionList(slots.otherArtistLinks);
+          }
+
+          if (!html.isBlank(slots.annotation)) {
+            anyAccent = true;
+            workingCapsule += '.withAnnotation';
+            workingOptions.annotation = slots.annotation;
+          }
+
+          if (anyAccent) {
+            return language.$(workingCapsule, workingOptions);
+          } else {
+            return slots.content;
+          }
+        }))),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
index f4413197..72bbf1b6 100644
--- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
@@ -216,53 +216,52 @@ export default {
           itemAnnotations,
           itemTypes,
         }) =>
-          (chunkType === 'album'
-            ? chunk.slots({
-                mode: 'album',
-                albumLink: chunkLink,
-                items:
-                  stitchArrays({
-                    item: items,
-                    link: itemLinks,
-                    annotation: itemAnnotations,
-                    type: itemTypes,
-                  }).map(({item, link, annotation, type}) =>
-                    item.slots({
-                      annotation:
-                        (annotation
-                          ? annotation.slot('mode', 'inline')
-                          : null),
-
-                      content:
-                        (type === 'album'
-                          ? html.tag('i',
-                              language.$('artistPage.creditList.entry.album.commentary'))
-                          : language.$('artistPage.creditList.entry.track', {
-                              track: link,
-                            })),
-                    })),
-              })
-         : chunkType === 'flash-act'
-            ? chunk.slots({
-                mode: 'flash',
-                flashActLink: chunkLink,
-                items:
-                  stitchArrays({
-                    item: items,
-                    link: itemLinks,
-                    annotation: itemAnnotations,
-                  }).map(({item, link, annotation}) =>
-                    item.slots({
-                      annotation:
-                        (annotation
-                          ? annotation.slot('mode', 'inline')
-                          : null),
-
-                      content:
-                        language.$('artistPage.creditList.entry.flash', {
-                          flash: link,
-                        }),
-                    })),
-              })
-            : null))),
+          language.encapsulate('artistPage.creditList.entry', capsule =>
+            (chunkType === 'album'
+              ? chunk.slots({
+                  mode: 'album',
+                  albumLink: chunkLink,
+                  items:
+                    stitchArrays({
+                      item: items,
+                      link: itemLinks,
+                      annotation: itemAnnotations,
+                      type: itemTypes,
+                    }).map(({item, link, annotation, type}) =>
+                      item.slots({
+                        annotation:
+                          (annotation
+                            ? annotation.slot('mode', 'inline')
+                            : null),
+
+                        content:
+                          (type === 'album'
+                            ? html.tag('i',
+                                language.$(capsule, 'album.commentary'))
+                            : language.$(capsule, 'track', {track: link})),
+                      })),
+                })
+           : chunkType === 'flash-act'
+              ? chunk.slots({
+                  mode: 'flash',
+                  flashActLink: chunkLink,
+                  items:
+                    stitchArrays({
+                      item: items,
+                      link: itemLinks,
+                      annotation: itemAnnotations,
+                    }).map(({item, link, annotation}) =>
+                      item.slots({
+                        annotation:
+                          (annotation
+                            ? annotation.slot('mode', 'inline')
+                            : null),
+
+                        content:
+                          language.$(capsule, 'flash', {
+                            flash: link,
+                          }),
+                      })),
+                })
+              : null)))),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
index d7460c80..d526f97a 100644
--- a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
@@ -100,13 +100,16 @@ export default {
           : html.blank()),
 
       content:
-        (data.duration
-          ? language.$('artistPage.creditList.entry.track.withDuration', {
-              track: relations.trackLink,
-              duration: language.formatDuration(data.duration),
-            })
-          : language.$('artistPage.creditList.entry.track', {
-              track: relations.trackLink,
-            })),
+        language.encapsulate('artistPage.creditList.entry.track', capsule => {
+          const options = {track: relations.trackLink};
+
+          if (data.duration) {
+            capsule += '.withDuration';
+            options.duration =
+              language.formatDuration(data.duration);
+          }
+
+          return language.$(capsule, options);
+        }),
     }),
 };
diff --git a/src/content/dependencies/generateChronologyLinksScopeSwitcher.js b/src/content/dependencies/generateChronologyLinksScopeSwitcher.js
index 23c44268..4a1b67a7 100644
--- a/src/content/dependencies/generateChronologyLinksScopeSwitcher.js
+++ b/src/content/dependencies/generateChronologyLinksScopeSwitcher.js
@@ -32,18 +32,19 @@ export default {
         {class: 'underline-white'},
 
         html.tag('span',
-          language.$('trackPage.nav.chronology.scope.title', {
-            scope:
-              slots.scopes.map((scope, index) =>
-                html.tag('a', {class: 'switcher-link'},
-                  {href: '#'},
+          language.encapsulate('trackPage.nav.chronology.scope', capsule =>
+            language.$(capsule, 'title', {
+              scope:
+                slots.scopes.map((scope, index) =>
+                  html.tag('a', {class: 'switcher-link'},
+                    {href: '#'},
 
-                  (index === 0
-                    ? {style: 'display: inline'}
-                    : {style: 'display: none'}),
+                    (index === 0
+                      ? {style: 'display: inline'}
+                      : {style: 'display: none'}),
 
-                  language.$('trackPage.nav.chronology.scope', scope))),
-          })));
+                    language.$(capsule, scope))),
+            }))));
 
     const scopeContents =
       stitchArrays({
diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js
index 036f8a6f..7c4aed80 100644
--- a/src/content/dependencies/generateCommentaryEntry.js
+++ b/src/content/dependencies/generateCommentaryEntry.js
@@ -43,60 +43,71 @@ export default {
     color: {validate: v => v.isColor},
   },
 
-  generate(data, relations, slots, {html, language}) {
-    const artistsSpan =
-      html.tag('span', {class: 'commentary-entry-artists'},
-        (relations.artistsContent
-          ? relations.artistsContent.slot('mode', 'inline')
-       : relations.artistLinks
-          ? language.formatConjunctionList(relations.artistLinks)
-          : language.$('misc.artistCommentary.entry.title.noArtists')));
-
-    const accentParts = ['misc.artistCommentary.entry.title.accent'];
-    const accentOptions = {};
-
-    if (relations.annotationContent) {
-      accentParts.push('withAnnotation');
-      accentOptions.annotation =
-        relations.annotationContent.slot('mode', 'inline');
-    }
-
-    const accent =
-      (accentParts.length > 1
-        ? html.tag('span', {class: 'commentary-entry-accent'},
-            language.$(...accentParts, accentOptions))
-        : null);
-
-    const titlePrefix = 'misc.artistCommentary.entry.title';
-    const titleParts = [titlePrefix];
-    const titleOptions = {artists: artistsSpan};
-
-    if (accent) {
-      titleParts.push('withAccent');
-      titleOptions.accent = accent;
-    }
-
-    const style =
-      slots.color &&
-        relations.colorStyle.slot('color', slots.color);
-
-    return html.tags([
-      html.tag('p', {class: 'commentary-entry-heading'},
-        style,
-        [
-          html.tag('time',
-            {[html.onlyIfContent]: true},
-            language.$(titlePrefix, 'date', {
-              [language.onlyIfOptions]: ['date'],
-              date: language.formatDate(data.date),
-            })),
-
-          language.$(...titleParts, titleOptions),
-        ]),
-
-      html.tag('blockquote', {class: 'commentary-entry-body'},
-        style,
-        relations.bodyContent.slot('mode', 'multiline')),
-    ]);
-  },
+  generate: (data, relations, slots, {html, language}) =>
+    language.encapsulate('misc.artistCommentary.entry', entryCapsule =>
+      html.tags([
+        html.tag('p', {class: 'commentary-entry-heading'},
+          slots.color &&
+            relations.colorStyle.clone()
+              .slot('color', slots.color),
+
+          language.encapsulate(entryCapsule, 'title', titleCapsule => [
+            html.tag('time',
+              {[html.onlyIfContent]: true},
+
+              language.$(titleCapsule, 'date', {
+                [language.onlyIfOptions]: ['date'],
+
+                date:
+                  language.formatDate(data.date),
+              })),
+
+            language.encapsulate(titleCapsule, workingCapsule => {
+              const workingOptions = {};
+
+              workingOptions.artists =
+                html.tag('span', {class: 'commentary-entry-artists'},
+                  (relations.artistsContent
+                    ? relations.artistsContent.slot('mode', 'inline')
+                 : relations.artistLinks
+                    ? language.formatConjunctionList(relations.artistLinks)
+                    : language.$(titleCapsule, 'noArtists')));
+
+              const accent =
+                html.tag('span', {class: 'commentary-entry-accent'},
+                  {[html.onlyIfContent]: true},
+
+                  language.encapsulate(titleCapsule, 'accent', accentCapsule =>
+                    language.encapsulate(accentCapsule, workingCapsule => {
+                      const workingOptions = {};
+
+                      if (relations.annotationContent) {
+                        workingCapsule += '.withAnnotation';
+                        workingOptions.annotation =
+                          relations.annotationContent.slot('mode', 'inline');
+                      }
+
+                      if (workingCapsule === accentCapsule) {
+                        return html.blank();
+                      } else {
+                        return language.$(workingCapsule, workingOptions);
+                      }
+                    })));
+
+              if (!html.isBlank(accent)) {
+                workingCapsule += '.withAccent';
+                workingOptions.accent = accent;
+              }
+
+              return language.$(workingCapsule, workingOptions);
+            }),
+          ])),
+
+        html.tag('blockquote', {class: 'commentary-entry-body'},
+          slots.color &&
+            relations.colorStyle.clone()
+              .slot('color', slots.color),
+
+          relations.bodyContent.slot('mode', 'multiline')),
+      ])),
 };
diff --git a/src/content/dependencies/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js
index 3c3504d2..d68ba42e 100644
--- a/src/content/dependencies/generateCommentaryIndexPage.js
+++ b/src/content/dependencies/generateCommentaryIndexPage.js
@@ -57,46 +57,48 @@ export default {
     };
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout.slots({
-      title: language.$('commentaryIndex.title'),
-
-      headingMode: 'static',
-
-      mainClasses: ['long-content'],
-      mainContent: [
-        html.tag('p', language.$('commentaryIndex.infoLine', {
-          words:
-            html.tag('b',
-              language.formatWordCount(data.totalWordCount, {unit: true})),
-
-          entries:
-            html.tag('b',
-                language.countCommentaryEntries(data.totalEntryCount, {unit: true})),
-        })),
-
-        html.tag('p',
-          language.$('commentaryIndex.albumList.title')),
-
-        html.tag('ul',
-          stitchArrays({
-            albumLink: relations.albumLinks,
-            wordCount: data.wordCounts,
-            entryCount: data.entryCounts,
-          }).map(({albumLink, wordCount, entryCount}) =>
-            html.tag('li',
-              language.$('commentaryIndex.albumList.item', {
-                album: albumLink,
-                words: language.formatWordCount(wordCount, {unit: true}),
-                entries: language.countCommentaryEntries(entryCount, {unit: true}),
-              })))),
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {auto: 'current'},
-      ],
-    });
-  },
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('commentaryIndex', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title'),
+
+        headingMode: 'static',
+
+        mainClasses: ['long-content'],
+        mainContent: [
+          html.tag('p', language.$(pageCapsule, 'infoLine', {
+            words:
+              html.tag('b',
+                language.formatWordCount(data.totalWordCount, {unit: true})),
+
+            entries:
+              html.tag('b',
+                  language.countCommentaryEntries(data.totalEntryCount, {unit: true})),
+          })),
+
+          language.encapsulate(pageCapsule, 'albumList', listCapsule => [
+            html.tag('p',
+              language.$(listCapsule, 'title')),
+
+            html.tag('ul',
+              stitchArrays({
+                albumLink: relations.albumLinks,
+                wordCount: data.wordCounts,
+                entryCount: data.entryCounts,
+              }).map(({albumLink, wordCount, entryCount}) =>
+                html.tag('li',
+                  language.$(listCapsule, 'item', {
+                    album: albumLink,
+                    words: language.formatWordCount(wordCount, {unit: true}),
+                    entries: language.countCommentaryEntries(entryCount, {unit: true}),
+                  })))),
+          ]),
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {auto: 'current'},
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js
index 17078124..1fa6de51 100644
--- a/src/content/dependencies/generateFlashActGalleryPage.js
+++ b/src/content/dependencies/generateFlashActGalleryPage.js
@@ -11,7 +11,7 @@ export default {
     'linkFlashIndex',
   ],
 
-  extraDependencies: ['html', 'language'],
+  extraDependencies: ['language'],
 
   relations: (relation, act) => ({
     layout:
@@ -50,42 +50,42 @@ export default {
         ['media.flashArt', flash.directory, flash.coverArtFileExtension])
   }),
 
-  generate(data, relations, {html, language}) {
-    return relations.layout.slots({
-      title:
-        language.$('flashPage.title', {
-          flash: new html.Tag(null, null, data.name),
-        }),
-
-      color: data.color,
-      headingMode: 'static',
-
-      mainClasses: ['flash-index'],
-      mainContent: [
-        relations.coverGrid.slots({
-          links: relations.flashLinks,
-          names: data.flashNames,
-          lazy: 6,
-
-          images:
-            stitchArrays({
-              image: relations.coverGridImages,
-              path: data.flashCoverPaths,
-            }).map(({image, path}) =>
-                image.slot('path', path)),
-        }),
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {html: relations.flashIndexLink},
-        {auto: 'current'},
-      ],
-
-      navBottomRowContent: relations.flashActNavAccent,
-
-      leftSidebar: relations.sidebar,
-    });
-  },
+  generate: (data, relations, {language}) =>
+    language.encapsulate('flashPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            flash: data.name,
+          }),
+
+        color: data.color,
+        headingMode: 'static',
+
+        mainClasses: ['flash-index'],
+        mainContent: [
+          relations.coverGrid.slots({
+            links: relations.flashLinks,
+            names: data.flashNames,
+            lazy: 6,
+
+            images:
+              stitchArrays({
+                image: relations.coverGridImages,
+                path: data.flashCoverPaths,
+              }).map(({image, path}) =>
+                  image.slot('path', path)),
+          }),
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {html: relations.flashIndexLink},
+          {auto: 'current'},
+        ],
+
+        navBottomRowContent: relations.flashActNavAccent,
+
+        leftSidebar: relations.sidebar,
+      })),
 };
diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js
index eaea7e9c..a21bb49e 100644
--- a/src/content/dependencies/generateFlashIndexPage.js
+++ b/src/content/dependencies/generateFlashIndexPage.js
@@ -81,76 +81,77 @@ export default {
   }),
 
   generate: (data, relations, {html, language}) =>
-    relations.layout.slots({
-      title: language.$('flashIndex.title'),
-      headingMode: 'static',
-
-      mainClasses: ['flash-index'],
-      mainContent: [
-        html.tags([
-          html.tag('p', {class: 'quick-info'},
-            {[html.onlyIfSiblings]: true},
-            language.$('misc.jumpTo')),
-
-          html.tag('ul', {class: 'quick-info'},
-            {[html.onlyIfContent]: true},
-            stitchArrays({
-              colorStyle: relations.jumpLinkColorStyles,
-              anchor: data.jumpLinkAnchors,
-              label: data.jumpLinkLabels,
-            }).map(({colorStyle, anchor, label}) =>
-                html.tag('li',
-                  html.tag('a',
-                    {href: '#' + anchor},
-                    colorStyle,
-                    label)))),
-        ]),
-
-        stitchArrays({
-          colorStyle: relations.actColorStyles,
-          actLink: relations.actLinks,
-          anchor: data.actAnchors,
-
-          coverGrid: relations.actCoverGrids,
-          coverGridImages: relations.actCoverGridImages,
-          coverGridLinks: relations.actCoverGridLinks,
-          coverGridNames: data.actCoverGridNames,
-          coverGridPaths: data.actCoverGridPaths,
-        }).map(({
-            colorStyle,
-            actLink,
-            anchor,
-
-            coverGrid,
-            coverGridImages,
-            coverGridLinks,
-            coverGridNames,
-            coverGridPaths,
-          }, index) => [
-            html.tag('h2',
-              {id: anchor},
-              colorStyle,
-              actLink),
-
-            coverGrid.slots({
-              links: coverGridLinks,
-              names: coverGridNames,
-              lazy: index === 0 ? 4 : true,
-
-              images:
-                stitchArrays({
-                  image: coverGridImages,
-                  path: coverGridPaths,
-                }).map(({image, path}) =>
-                    image.slot('path', path)),
-            }),
+    language.encapsulate('flashIndex', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title'),
+        headingMode: 'static',
+
+        mainClasses: ['flash-index'],
+        mainContent: [
+          html.tags([
+            html.tag('p', {class: 'quick-info'},
+              {[html.onlyIfSiblings]: true},
+              language.$('misc.jumpTo')),
+
+            html.tag('ul', {class: 'quick-info'},
+              {[html.onlyIfContent]: true},
+              stitchArrays({
+                colorStyle: relations.jumpLinkColorStyles,
+                anchor: data.jumpLinkAnchors,
+                label: data.jumpLinkLabels,
+              }).map(({colorStyle, anchor, label}) =>
+                  html.tag('li',
+                    html.tag('a',
+                      {href: '#' + anchor},
+                      colorStyle,
+                      label)))),
           ]),
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {auto: 'current'},
-      ],
-    }),
+
+          stitchArrays({
+            colorStyle: relations.actColorStyles,
+            actLink: relations.actLinks,
+            anchor: data.actAnchors,
+
+            coverGrid: relations.actCoverGrids,
+            coverGridImages: relations.actCoverGridImages,
+            coverGridLinks: relations.actCoverGridLinks,
+            coverGridNames: data.actCoverGridNames,
+            coverGridPaths: data.actCoverGridPaths,
+          }).map(({
+              colorStyle,
+              actLink,
+              anchor,
+
+              coverGrid,
+              coverGridImages,
+              coverGridLinks,
+              coverGridNames,
+              coverGridPaths,
+            }, index) => [
+              html.tag('h2',
+                {id: anchor},
+                colorStyle,
+                actLink),
+
+              coverGrid.slots({
+                links: coverGridLinks,
+                names: coverGridNames,
+                lazy: index === 0 ? 4 : true,
+
+                images:
+                  stitchArrays({
+                    image: coverGridImages,
+                    path: coverGridPaths,
+                  }).map(({image, path}) =>
+                      image.slot('path', path)),
+              }),
+            ]),
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {auto: 'current'},
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js
index d6066a95..96337d83 100644
--- a/src/content/dependencies/generateFlashInfoPage.js
+++ b/src/content/dependencies/generateFlashInfoPage.js
@@ -77,86 +77,91 @@ export default {
   }),
 
   generate: (data, relations, {html, language}) =>
-    relations.layout.slots({
-      title:
-        language.$('flashPage.title', {
-          flash: data.name,
-        }),
-
-      color: data.color,
-      headingMode: 'sticky',
-
-      cover:
-        (relations.cover
-          ? relations.cover.slots({
-              alt: language.$('misc.alt.flashArt'),
-            })
-          : null),
-
-      mainContent: [
-        html.tag('p',
-          language.$('releaseInfo.released', {
-            date: language.formatDate(data.date),
-          })),
-
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          language.$('releaseInfo.playOn', {
-            [language.onlyIfOptions]: ['links'],
-            links:
-              language.formatDisjunctionList(
-                relations.externalLinks
-                  .map(link => link.slot('context', 'flash'))),
-          })),
-
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          {[html.joinChildren]: html.tag('br')},
-
-          [
-            !html.isBlank(relations.artistCommentarySection) &&
-              language.$('releaseInfo.readCommentary', {
-                link: html.tag('a',
-                  {href: '#artist-commentary'},
-                  language.$('releaseInfo.readCommentary.link')),
+    language.encapsulate('flashPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            flash: data.name,
+          }),
+
+        color: data.color,
+        headingMode: 'sticky',
+
+        cover:
+          (relations.cover
+            ? relations.cover.slots({
+                alt: language.$('misc.alt.flashArt'),
+              })
+            : null),
+
+        mainContent: [
+          html.tag('p',
+            language.$('releaseInfo.released', {
+              date: language.formatDate(data.date),
+            })),
+
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+
+            language.$('releaseInfo.playOn', {
+              [language.onlyIfOptions]: ['links'],
+
+              links:
+                language.formatDisjunctionList(
+                  relations.externalLinks
+                    .map(link => link.slot('context', 'flash'))),
+            })),
+
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+            {[html.joinChildren]: html.tag('br')},
+
+            language.encapsulate('releaseInfo', capsule => [
+              !html.isBlank(relations.artistCommentarySection) &&
+                language.encapsulate(capsule, 'readCommentary', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#artist-commentary'},
+                        language.$(capsule, 'link')),
+                  })),
+            ])),
+
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'features'},
+                title:
+                  language.$('releaseInfo.tracksFeatured', {
+                    flash: html.tag('i', data.name),
+                  }),
               }),
+
+            relations.featuredTracksList,
+          ]),
+
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'contributors'},
+                title: language.$('releaseInfo.contributors'),
+              }),
+
+            relations.contributorContributionList,
           ]),
 
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'features'},
-              title:
-                language.$('releaseInfo.tracksFeatured', {
-                  flash: html.tag('i', data.name),
-                }),
-            }),
-
-          relations.featuredTracksList,
-        ]),
-
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'contributors'},
-              title: language.$('releaseInfo.contributors'),
-            }),
-
-          relations.contributorContributionList,
-        ]),
-
-        relations.artistCommentarySection,
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {html: relations.flashActLink.slot('color', false)},
-        {auto: 'current'},
-      ],
-
-      navBottomRowContent: relations.flashNavAccent,
-
-      leftSidebar: relations.sidebar,
-    }),
+          relations.artistCommentarySection,
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {html: relations.flashActLink.slot('color', false)},
+          {auto: 'current'},
+        ],
+
+        navBottomRowContent: relations.flashNavAccent,
+
+        leftSidebar: relations.sidebar,
+      })),
 };
diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js
index 34c789b3..ceb54322 100644
--- a/src/content/dependencies/generateGroupGalleryPage.js
+++ b/src/content/dependencies/generateGroupGalleryPage.js
@@ -111,10 +111,10 @@ export default {
     return data;
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout
-      .slots({
-        title: language.$('groupGalleryPage.title', {group: data.name}),
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('groupGalleryPage', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title', {group: data.name}),
         headingMode: 'static',
 
         color: data.color,
@@ -135,7 +135,7 @@ export default {
           relations.quickDescription,
 
           html.tag('p', {class: 'quick-info'},
-            language.$('groupGalleryPage.infoLine', {
+            language.$(pageCapsule, 'infoLine', {
               tracks:
                 html.tag('b',
                   language.countTracks(data.numTracks, {
@@ -199,6 +199,5 @@ export default {
 
         secondaryNav:
           relations.secondaryNav ?? null,
-      });
-  },
+      })),
 };
diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js
index 956d56d8..87f35656 100644
--- a/src/content/dependencies/generateGroupInfoPage.js
+++ b/src/content/dependencies/generateGroupInfoPage.js
@@ -53,38 +53,41 @@ export default {
   }),
 
   generate: (data, relations, {html, language}) =>
-    relations.layout.slots({
-      title: language.$('groupInfoPage.title', {group: data.name}),
-      headingMode: 'sticky',
-      color: data.color,
-
-      mainContent: [
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          language.$('releaseInfo.visitOn', {
-            [language.onlyIfOptions]: ['links'],
-            links:
-              language.formatDisjunctionList(
-                relations.visitLinks
-                  .map(link => link.slot('context', 'group'))),
-          })),
-
-        html.tag('blockquote',
-          {[html.onlyIfContent]: true},
-          relations.description.slot('mode', 'multiline')),
-
-        relations.albumSection,
-      ],
-
-      leftSidebar:
-        (relations.sidebar
-          ? relations.sidebar
-              .content /* TODO: Kludge. */
-          : null),
-
-      navLinkStyle: 'hierarchical',
-      navLinks: relations.navLinks.content,
-
-      secondaryNav: relations.secondaryNav ?? null,
-    }),
+    language.encapsulate('groupInfoPage', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title', {group: data.name}),
+        headingMode: 'sticky',
+        color: data.color,
+
+        mainContent: [
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+
+            language.$('releaseInfo.visitOn', {
+              [language.onlyIfOptions]: ['links'],
+
+              links:
+                language.formatDisjunctionList(
+                  relations.visitLinks
+                    .map(link => link.slot('context', 'group'))),
+            })),
+
+          html.tag('blockquote',
+            {[html.onlyIfContent]: true},
+            relations.description.slot('mode', 'multiline')),
+
+          relations.albumSection,
+        ],
+
+        leftSidebar:
+          (relations.sidebar
+            ? relations.sidebar
+                .content /* TODO: Kludge. */
+            : null),
+
+        navLinkStyle: 'hierarchical',
+        navLinks: relations.navLinks.content,
+
+        secondaryNav: relations.secondaryNav ?? null,
+      })),
 };
diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js
index d1dab542..8899e98e 100644
--- a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js
+++ b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js
@@ -66,67 +66,71 @@ export default {
   }),
 
   generate: (relations, {html, language}) =>
-    html.tags([
-      relations.contentHeading
-        .slots({
-          tag: 'h2',
-          title: language.$('groupInfoPage.albumList.title'),
-        }),
-
-      html.tag('p',
-        {[html.onlyIfSiblings]: true},
-        language.$('groupInfoPage.viewAlbumGallery', {
-          link:
-            relations.galleryLink
-              .slot('content', language.$('groupInfoPage.viewAlbumGallery.link')),
-        })),
-
-      html.tag('ul',
-        {[html.onlyIfContent]: true},
-
-        stitchArrays({
-          albumLink: relations.albumLinks,
-          otherGroupLinks: relations.otherGroupLinks,
-          datetimestamp: relations.datetimestamps,
-          albumColorStyle: relations.albumColorStyles,
-        }).map(({
-            albumLink,
-            otherGroupLinks,
-            datetimestamp,
-            albumColorStyle,
-          }) => {
-            const prefix = 'groupInfoPage.albumList.item';
-            const parts = [prefix];
-            const options = {};
-
-            options.album =
-              albumLink.slot('color', false);
-
-            if (datetimestamp) {
-              parts.push('withYear');
-              options.yearAccent =
-                language.$(prefix, 'yearAccent', {
-                  year:
-                    datetimestamp.slots({style: 'year', tooltip: true}),
-                });
-            }
-
-            if (!empty(otherGroupLinks)) {
-              parts.push('withOtherGroup');
-              options.otherGroupAccent =
-                html.tag('span', {class: 'other-group-accent'},
-                  language.$(prefix, 'otherGroupAccent', {
-                    groups:
-                      language.formatConjunctionList(
-                        otherGroupLinks.map(groupLink =>
-                          groupLink.slot('color', false))),
-                  }));
-            }
-
-            return (
-              html.tag('li',
+    language.encapsulate('groupInfoPage', pageCapsule =>
+      language.encapsulate(pageCapsule, 'albumList', listCapsule =>
+        html.tags([
+          relations.contentHeading
+            .slots({
+              tag: 'h2',
+              title: language.$(listCapsule, 'title'),
+            }),
+
+          html.tag('p',
+            {[html.onlyIfSiblings]: true},
+
+            language.encapsulate(pageCapsule, 'viewAlbumGallery', capsule =>
+              language.$(capsule, {
+                link:
+                  relations.galleryLink
+                    .slot('content', language.$(capsule, 'link')),
+              }))),
+
+          html.tag('ul',
+            {[html.onlyIfContent]: true},
+
+            stitchArrays({
+              albumLink: relations.albumLinks,
+              otherGroupLinks: relations.otherGroupLinks,
+              datetimestamp: relations.datetimestamps,
+              albumColorStyle: relations.albumColorStyles,
+            }).map(({
+                albumLink,
+                otherGroupLinks,
+                datetimestamp,
                 albumColorStyle,
-                language.$(...parts, options)));
-          })),
-    ]),
+              }) =>
+                html.tag('li',
+                  albumColorStyle,
+
+                  language.encapsulate(listCapsule, 'item', itemCapsule =>
+                    language.encapsulate(itemCapsule, workingCapsule => {
+                      const workingOptions = {};
+
+                      workingOptions.album =
+                        albumLink.slot('color', false);
+
+                      if (datetimestamp) {
+                        workingCapsule += '.withYear';
+                        workingOptions.yearAccent =
+                          language.$(itemCapsule, 'yearAccent', {
+                            year:
+                              datetimestamp.slots({style: 'year', tooltip: true}),
+                          });
+                      }
+
+                      if (!empty(otherGroupLinks)) {
+                        workingCapsule += '.withOtherGroup';
+                        workingOptions.otherGroupAccent =
+                          html.tag('span', {class: 'other-group-accent'},
+                            language.$(itemCapsule, 'otherGroupAccent', {
+                              groups:
+                                language.formatConjunctionList(
+                                  otherGroupLinks.map(groupLink =>
+                                    groupLink.slot('color', false))),
+                            }));
+                      }
+
+                      return language.$(workingCapsule, workingOptions);
+                    }))))),
+        ]))),
 };
diff --git a/src/content/dependencies/generateGroupSidebarCategoryDetails.js b/src/content/dependencies/generateGroupSidebarCategoryDetails.js
index 69de373b..d52c77b8 100644
--- a/src/content/dependencies/generateGroupSidebarCategoryDetails.js
+++ b/src/content/dependencies/generateGroupSidebarCategoryDetails.js
@@ -46,37 +46,37 @@ export default {
     },
   },
 
-  generate(data, relations, slots, {html, language}) {
-    return html.tag('details',
-      data.isCurrentCategory &&
-        {class: 'current', open: true},
-
-      [
-        html.tag('summary',
-          relations.colorStyle,
-
-          html.tag('span',
-            language.$('groupSidebar.groupList.category', {
-              category:
-                html.tag('span', {class: 'group-name'},
-                  data.name),
-            }))),
-
-        html.tag('ul',
-          stitchArrays(({
-            infoLink: relations.groupInfoLinks,
-            galleryLink: relations.groupGalleryLinks,
-          })).map(({infoLink, galleryLink}, index) =>
-                html.tag('li',
-                  index === data.currentGroupIndex &&
-                    {class: 'current'},
-
-                  language.$('groupSidebar.groupList.item', {
-                    group:
-                      (slots.currentExtra === 'gallery'
-                        ? galleryLink ?? infoLink
-                        : infoLink),
-                  })))),
-      ]);
-  },
+  generate: (data, relations, slots, {html, language}) =>
+    language.encapsulate('groupSidebar.groupList', capsule =>
+      html.tag('details',
+        data.isCurrentCategory &&
+          {class: 'current', open: true},
+
+        [
+          html.tag('summary',
+            relations.colorStyle,
+
+            html.tag('span',
+              language.$(capsule, 'category', {
+                category:
+                  html.tag('span', {class: 'group-name'},
+                    data.name),
+              }))),
+
+          html.tag('ul',
+            stitchArrays(({
+              infoLink: relations.groupInfoLinks,
+              galleryLink: relations.groupGalleryLinks,
+            })).map(({infoLink, galleryLink}, index) =>
+                  html.tag('li',
+                    index === data.currentGroupIndex &&
+                      {class: 'current'},
+
+                    language.$(capsule, 'item', {
+                      group:
+                        (slots.currentExtra === 'gallery'
+                          ? galleryLink ?? infoLink
+                          : infoLink),
+                    })))),
+        ])),
 };
diff --git a/src/content/dependencies/generateNewsEntryPage.js b/src/content/dependencies/generateNewsEntryPage.js
index bcba7194..2c382cfa 100644
--- a/src/content/dependencies/generateNewsEntryPage.js
+++ b/src/content/dependencies/generateNewsEntryPage.js
@@ -91,41 +91,41 @@ export default {
     };
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout.slots({
-      title:
-        language.$('newsEntryPage.title', {
-          entry: data.name,
-        }),
-
-      headingMode: 'sticky',
-
-      mainClasses: ['long-content'],
-      mainContent: [
-        html.tag('p',
-          language.$('newsEntryPage.published', {
-            date: language.formatDate(data.date),
-          })),
-
-        relations.content,
-        relations.readAnotherLinks,
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {html: relations.newsIndexLink},
-        {
-          auto: 'current',
-          accent:
-            (relations.previousNextLinks
-              ? `(${language.formatUnitList(relations.previousNextLinks.slots({
-                  previousLink: relations.previousEntryNavLink ?? null,
-                  nextLink: relations.nextEntryNavLink ?? null,
-                }).content)})`
-              : null),
-        },
-      ],
-    });
-  },
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('newsEntryPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            entry: data.name,
+          }),
+
+        headingMode: 'sticky',
+
+        mainClasses: ['long-content'],
+        mainContent: [
+          html.tag('p',
+            language.$(pageCapsule, 'published', {
+              date: language.formatDate(data.date),
+            })),
+
+          relations.content,
+          relations.readAnotherLinks,
+        ],
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {html: relations.newsIndexLink},
+          {
+            auto: 'current',
+            accent:
+              (relations.previousNextLinks
+                ? `(${language.formatUnitList(relations.previousNextLinks.slots({
+                    previousLink: relations.previousEntryNavLink ?? null,
+                    nextLink: relations.nextEntryNavLink ?? null,
+                  }).content)})`
+                : null),
+          },
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateNewsIndexPage.js b/src/content/dependencies/generateNewsIndexPage.js
index 539af804..02964ce8 100644
--- a/src/content/dependencies/generateNewsIndexPage.js
+++ b/src/content/dependencies/generateNewsIndexPage.js
@@ -57,37 +57,38 @@ export default {
     };
   },
 
-  generate(data, relations, {html, language}) {
-    return relations.layout.slots({
-      title: language.$('newsIndex.title'),
-      headingMode: 'sticky',
-
-      mainClasses: ['long-content', 'news-index'],
-      mainContent:
-        stitchArrays({
-          entryLink: relations.entryLinks,
-          viewRestLink: relations.viewRestLinks,
-          content: relations.entryContents,
-          date: data.entryDates,
-          directory: data.entryDirectories,
-        }).map(({entryLink, viewRestLink, content, date, directory}) =>
-            html.tag('article', {id: directory}, [
-              html.tag('h2', [
-                html.tag('time', language.formatDate(date)),
-                entryLink,
-              ]),
-
-              content,
-
-              viewRestLink
-                ?.slot('content', language.$('newsIndex.entry.viewRest')),
-            ])),
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {auto: 'current'},
-      ],
-    });
-  },
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('newsIndex', pageCapsule =>
+      relations.layout.slots({
+        title: language.$(pageCapsule, 'title'),
+        headingMode: 'sticky',
+
+        mainClasses: ['long-content', 'news-index'],
+        mainContent:
+          stitchArrays({
+            entryLink: relations.entryLinks,
+            viewRestLink: relations.viewRestLinks,
+            content: relations.entryContents,
+            date: data.entryDates,
+            directory: data.entryDirectories,
+          }).map(({entryLink, viewRestLink, content, date, directory}) =>
+              language.encapsulate(pageCapsule, 'entry', entryCapsule =>
+                html.tag('article', {id: directory}, [
+                  html.tag('h2', [
+                    html.tag('time', language.formatDate(date)),
+                    entryLink,
+                  ]),
+
+                  content,
+
+                  viewRestLink
+                    ?.slot('content', language.$(entryCapsule, 'viewRest')),
+                ]))),
+
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {auto: 'current'},
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index e138a981..7e9e49a0 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -506,41 +506,43 @@ export default {
           html.tag('img', {id: 'image-overlay-image'}),
           html.tag('img', {id: 'image-overlay-image-thumb'}),
         ]),
-        html.tag('div', {id: 'image-overlay-action-container'}, [
-          html.tag('div', {id: 'image-overlay-action-content-without-size'},
-            language.$('releaseInfo.viewOriginalFile', {
-              link: html.tag('a', {class: 'image-overlay-view-original'},
-                language.$('releaseInfo.viewOriginalFile.link')),
-            })),
-
-          html.tag('div', {id: 'image-overlay-action-content-with-size'}, [
-            language.$('releaseInfo.viewOriginalFile.withSize', {
-              link:
-                html.tag('a', {class: 'image-overlay-view-original'},
-                  language.$('releaseInfo.viewOriginalFile.link')),
-
-              size:
-                html.tag('span',
-                  {[html.joinChildren]: ''},
-                  [
-                    html.tag('span', {id: 'image-overlay-file-size-kilobytes'},
-                      language.$('count.fileSize.kilobytes', {
-                        kilobytes:
-                          html.tag('span', {class: 'image-overlay-file-size-count'}),
-                      })),
-
-                    html.tag('span', {id: 'image-overlay-file-size-megabytes'},
-                      language.$('count.fileSize.megabytes', {
-                        megabytes:
-                          html.tag('span', {class: 'image-overlay-file-size-count'}),
-                      })),
-                  ]),
-            }),
 
-            html.tag('span', {id: 'image-overlay-file-size-warning'},
-              language.$('releaseInfo.viewOriginalFile.sizeWarning')),
-          ]),
-        ]),
+        html.tag('div', {id: 'image-overlay-action-container'},
+          language.encapsulate('releaseInfo.viewOriginalFile', capsule => [
+            html.tag('div', {id: 'image-overlay-action-content-without-size'},
+              language.$(capsule, {
+                link: html.tag('a', {class: 'image-overlay-view-original'},
+                  language.$(capsule, 'link')),
+              })),
+
+            html.tag('div', {id: 'image-overlay-action-content-with-size'}, [
+              language.$(capsule, 'withSize', {
+                link:
+                  html.tag('a', {class: 'image-overlay-view-original'},
+                    language.$(capsule, 'link')),
+
+                size:
+                  html.tag('span',
+                    {[html.joinChildren]: ''},
+                    [
+                      html.tag('span', {id: 'image-overlay-file-size-kilobytes'},
+                        language.$('count.fileSize.kilobytes', {
+                          kilobytes:
+                            html.tag('span', {class: 'image-overlay-file-size-count'}),
+                        })),
+
+                      html.tag('span', {id: 'image-overlay-file-size-megabytes'},
+                        language.$('count.fileSize.megabytes', {
+                          megabytes:
+                            html.tag('span', {class: 'image-overlay-file-size-count'}),
+                        })),
+                    ]),
+              }),
+
+              html.tag('span', {id: 'image-overlay-file-size-warning'},
+                language.$(capsule, 'sizeWarning')),
+            ]),
+          ])),
       ]));
 
     const layoutHTML = [
diff --git a/src/content/dependencies/generateSearchSidebarBox.js b/src/content/dependencies/generateSearchSidebarBox.js
index 6607c789..8b18cfae 100644
--- a/src/content/dependencies/generateSearchSidebarBox.js
+++ b/src/content/dependencies/generateSearchSidebarBox.js
@@ -8,50 +8,53 @@ export default {
   }),
 
   generate: (relations, {html, language}) =>
-    relations.sidebarBox.slots({
-      attributes: {class: 'wiki-search-sidebar-box'},
-      collapsible: false,
+    language.encapsulate('misc.search', capsule =>
+      relations.sidebarBox.slots({
+        attributes: {class: 'wiki-search-sidebar-box'},
+        collapsible: false,
 
-      content: [
-        html.tag('input', {class: 'wiki-search-input'},
-          {
-            placeholder:
-              language.$('misc.search.placeholder').toString(),
-          },
-          {type: 'search'}),
+        content: [
+          html.tag('input', {class: 'wiki-search-input'},
+            {
+              placeholder:
+                language.$(capsule, 'placeholder').toString(),
+            },
+            {type: 'search'}),
 
-        html.tag('template', {class: 'wiki-search-preparing-string'},
-          language.$('misc.search.preparing')),
+          html.tag('template', {class: 'wiki-search-preparing-string'},
+            language.$(capsule, 'preparing')),
 
-        html.tag('template', {class: 'wiki-search-loading-data-string'},
-          language.$('misc.search.loadingData')),
+          html.tag('template', {class: 'wiki-search-loading-data-string'},
+            language.$(capsule, 'loadingData')),
 
-        html.tag('template', {class: 'wiki-search-searching-string'},
-          language.$('misc.search.searching')),
+          html.tag('template', {class: 'wiki-search-searching-string'},
+            language.$(capsule, 'searching')),
 
-        html.tag('template', {class: 'wiki-search-failed-string'},
-          language.$('misc.search.failed')),
+          html.tag('template', {class: 'wiki-search-failed-string'},
+            language.$(capsule, 'failed')),
 
-        html.tag('template', {class: 'wiki-search-no-results-string'},
-          language.$('misc.search.noResults')),
+          html.tag('template', {class: 'wiki-search-no-results-string'},
+            language.$(capsule, 'noResults')),
 
-        html.tag('template', {class: 'wiki-search-current-result-string'},
-          language.$('misc.search.currentResult')),
+          html.tag('template', {class: 'wiki-search-current-result-string'},
+            language.$(capsule, 'currentResult')),
 
-        html.tag('template', {class: 'wiki-search-end-search-string'},
-          language.$('misc.search.endSearch')),
+          html.tag('template', {class: 'wiki-search-end-search-string'},
+            language.$(capsule, 'endSearch')),
 
-        html.tag('template', {class: 'wiki-search-album-result-kind-string'},
-          language.$('misc.search.resultKind.album')),
+          language.encapsulate(capsule, 'resultKind', capsule => [
+            html.tag('template', {class: 'wiki-search-album-result-kind-string'},
+              language.$(capsule, 'album')),
 
-        html.tag('template', {class: 'wiki-search-artist-result-kind-string'},
-          language.$('misc.search.resultKind.artist')),
+            html.tag('template', {class: 'wiki-search-artist-result-kind-string'},
+              language.$(capsule, 'artist')),
 
-        html.tag('template', {class: 'wiki-search-group-result-kind-string'},
-          language.$('misc.search.resultKind.group')),
+            html.tag('template', {class: 'wiki-search-group-result-kind-string'},
+              language.$(capsule, 'group')),
 
-        html.tag('template', {class: 'wiki-search-tag-result-kind-string'},
-          language.$('misc.search.resultKind.artTag')),
-      ],
-    }),
+            html.tag('template', {class: 'wiki-search-tag-result-kind-string'},
+              language.$(capsule, 'artTag')),
+          ]),
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index 1cbbc8a8..95eaf35b 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -134,251 +134,278 @@ export default {
   }),
 
   generate: (data, relations, {html, language}) =>
-    relations.layout.slots({
-      title: language.$('trackPage.title', {track: data.name}),
-      headingMode: 'sticky',
-
-      additionalNames: relations.additionalNamesBox,
-
-      color: data.color,
-      styleRules: [relations.albumStyleRules],
-
-      cover:
-        (relations.cover
-          ? relations.cover.slots({
-              alt: language.$('misc.alt.trackCover'),
-            })
-          : null),
-
-      mainContent: [
-        relations.releaseInfo,
-
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          {[html.joinChildren]: html.tag('br')},
-
-          [
-            !html.isBlank(relations.sheetMusicFilesList) &&
-              language.$('releaseInfo.sheetMusicFiles.shortcut', {
-                link: html.tag('a',
-                  {href: '#sheet-music-files'},
-                  language.$('releaseInfo.sheetMusicFiles.shortcut.link')),
-              }),
+    language.encapsulate('trackPage', pageCapsule =>
+      relations.layout.slots({
+        title:
+          language.$(pageCapsule, 'title', {
+            track: data.name,
+          }),
 
-            !html.isBlank(relations.midiProjectFilesList) &&
-              language.$('releaseInfo.midiProjectFiles.shortcut', {
-                link: html.tag('a',
-                  {href: '#midi-project-files'},
-                  language.$('releaseInfo.midiProjectFiles.shortcut.link')),
-              }),
+        headingMode: 'sticky',
 
-            !html.isBlank(relations.additionalFilesList) &&
-              language.$('releaseInfo.additionalFiles.shortcut', {
-                link: html.tag('a',
-                  {href: '#midi-project-files'},
-                  language.$('releaseInfo.additionalFiles.shortcut.link')),
-              }),
+        additionalNames: relations.additionalNamesBox,
+
+        color: data.color,
+        styleRules: [relations.albumStyleRules],
+
+        cover:
+          (relations.cover
+            ? relations.cover.slots({
+                alt: language.$('misc.alt.trackCover'),
+              })
+            : null),
+
+        mainContent: [
+          relations.releaseInfo,
 
-            !html.isBlank(relations.artistCommentarySection) &&
-              language.$('releaseInfo.readCommentary', {
-                link: html.tag('a',
-                  {href: '#artist-commentary'},
-                  language.$('releaseInfo.readCommentary.link')),
+          html.tag('p',
+            {[html.onlyIfContent]: true},
+            {[html.joinChildren]: html.tag('br')},
+
+            language.encapsulate('releaseInfo', capsule => [
+              !html.isBlank(relations.sheetMusicFilesList) &&
+                language.encapsulate(capsule, 'sheetMusicFiles.shortcut', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#sheet-music-files'},
+                        language.$(capsule, 'link')),
+                  })),
+
+              !html.isBlank(relations.midiProjectFilesList) &&
+                language.encapsulate(capsule, 'midiProjectFiles.shortcut', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#midi-project-files'},
+                        language.$(capsule, 'link')),
+                  })),
+
+              !html.isBlank(relations.additionalFilesList) &&
+                language.encapsulate(capsule, 'additionalFiles.shortcut', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#midi-project-files'},
+                        language.$(capsule, 'link')),
+                  })),
+
+              !html.isBlank(relations.artistCommentarySection) &&
+                language.encapsulate(capsule, 'readCommentary', capsule =>
+                  language.$(capsule, {
+                    link:
+                      html.tag('a',
+                        {href: '#artist-commentary'},
+                        language.$(capsule, 'link')),
+                  })),
+            ])),
+
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'also-released-as'},
+                title: language.$('releaseInfo.alsoReleasedAs'),
               }),
+
+            relations.otherReleasesList,
           ]),
 
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'also-released-as'},
-              title: language.$('releaseInfo.alsoReleasedAs'),
-            }),
-
-          relations.otherReleasesList,
-        ]),
-
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'contributors'},
-              title: language.$('releaseInfo.contributors'),
-            }),
-
-          relations.contributorContributionList,
-        ]),
-
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'references'},
-
-              title:
-                language.$('releaseInfo.tracksReferenced', {
-                  track: html.tag('i', data.name),
-                }),
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'contributors'},
+                title: language.$('releaseInfo.contributors'),
+              }),
 
-              stickyTitle:
-                language.$('releaseInfo.tracksReferenced.sticky'),
-            }),
+            relations.contributorContributionList,
+          ]),
 
-          relations.referencedTracksList,
-        ]),
+          html.tags([
+            language.encapsulate('releaseInfo.tracksReferenced', capsule =>
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'references'},
 
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'samples'},
+                  title:
+                    language.$(capsule, {
+                      track:
+                        html.tag('i', data.name),
+                    }),
 
-              title:
-                language.$('releaseInfo.tracksSampled', {
-                  track: html.tag('i', data.name),
-                }),
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
+                })),
 
-              stickyTitle:
-                language.$('releaseInfo.tracksSampled.sticky'),
-            }),
+            relations.referencedTracksList,
+          ]),
 
-          relations.sampledTracksList,
-        ]),
+          html.tags([
+            language.encapsulate('releaseInfo.tracksSampled', capsule =>
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'samples'},
 
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'referenced-by'},
+                  title:
+                    language.$(capsule, {
+                      track:
+                        html.tag('i', data.name),
+                    }),
 
-              title:
-                language.$('releaseInfo.tracksThatReference', {
-                  track: html.tag('i', data.name),
-                }),
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
+                })),
 
-              stickyTitle:
-                language.$('releaseInfo.tracksThatReference.sticky'),
-            }),
+            relations.sampledTracksList,
+          ]),
 
-          relations.referencedByTracksList
-            .slots({
-              headingString: 'releaseInfo.tracksThatReference',
-            }),
-        ]),
+          language.encapsulate('releaseInfo.tracksThatReference', capsule =>
+            html.tags([
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'referenced-by'},
 
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'sampled-by'},
+                  title:
+                    language.$(capsule, {
+                      track: html.tag('i', data.name),
+                    }),
 
-              title:
-                language.$('releaseInfo.tracksThatSample', {
-                  track: html.tag('i', data.name),
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
                 }),
 
-              stickyTitle:
-                language.$('releaseInfo.tracksThatSample.sticky'),
-            }),
+              relations.referencedByTracksList
+                .slots({
+                  headingString: capsule,
+                }),
+            ])),
 
-          relations.sampledByTracksList
-            .slots({
-              headingString: 'releaseInfo.tracksThatSample',
-            }),
-        ]),
+          language.encapsulate('releaseInfo.tracksThatSample', capsule =>
+            html.tags([
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'sampled-by'},
 
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'featured-in'},
+                  title:
+                    language.$(capsule, {
+                      track: html.tag('i', data.name),
+                    }),
 
-              title:
-                language.$('releaseInfo.flashesThatFeature', {
-                  track: html.tag('i', data.name),
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
                 }),
 
-              stickyTitle:
-                language.$('releaseInfo.flashesThatFeature.sticky'),
-            }),
+              relations.sampledByTracksList
+                .slots({
+                  headingString: capsule,
+                }),
+            ])),
 
-          relations.flashesThatFeatureList,
-        ]),
+          html.tags([
+            language.encapsulate('releaseInfo.flashesThatFeature', capsule =>
+              relations.contentHeading.clone()
+                .slots({
+                  attributes: {id: 'featured-in'},
 
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'lyrics'},
-              title: language.$('releaseInfo.lyrics'),
-            }),
+                  title:
+                    language.$(capsule, {
+                      track: html.tag('i', data.name),
+                    }),
 
-          html.tag('blockquote',
-            {[html.onlyIfContent]: true},
-            relations.lyrics.slot('mode', 'lyrics')),
-        ]),
-
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'sheet-music-files'},
-              title: language.$('releaseInfo.sheetMusicFiles.heading'),
-            }),
-
-          relations.sheetMusicFilesList,
-        ]),
-
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'midi-project-files'},
-              title: language.$('releaseInfo.midiProjectFiles.heading'),
-            }),
-
-          relations.midiProjectFilesList,
-        ]),
-
-        html.tags([
-          relations.contentHeading.clone()
-            .slots({
-              attributes: {id: 'additional-files'},
-              title: language.$('releaseInfo.additionalFiles.heading'),
-            }),
-
-          relations.additionalFilesList,
-        ]),
-
-        relations.artistCommentarySection,
-      ],
-
-      navLinkStyle: 'hierarchical',
-      navLinks: [
-        {auto: 'home'},
-        {html: relations.albumLink.slot('color', false)},
-        {
-          html:
-            (data.hasTrackNumbers
-              ? language.$('trackPage.nav.track.withNumber', {
-                  number: data.trackNumber,
-                  track: relations.trackLink
-                    .slot('attributes', {class: 'current'}),
-                })
-              : language.$('trackPage.nav.track', {
-                  track: relations.trackLink
-                    .slot('attributes', {class: 'current'}),
+                  stickyTitle:
+                    language.$(capsule, 'sticky'),
                 })),
-        },
-      ],
 
-      navBottomRowContent:
-        relations.albumNavAccent.slots({
-          showTrackNavigation: true,
-          showExtraLinks: false,
-        }),
+            relations.flashesThatFeatureList,
+          ]),
 
-      navContent:
-        relations.chronologyLinks,
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'lyrics'},
+                title: language.$('releaseInfo.lyrics'),
+              }),
 
-      secondaryNav:
-        relations.secondaryNav
-          .slot('mode', 'track'),
+            html.tag('blockquote',
+              {[html.onlyIfContent]: true},
+              relations.lyrics.slot('mode', 'lyrics')),
+          ]),
 
-      leftSidebar: relations.sidebar,
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'sheet-music-files'},
+                title: language.$('releaseInfo.sheetMusicFiles.heading'),
+              }),
 
-      socialEmbed: relations.socialEmbed,
-    }),
+            relations.sheetMusicFilesList,
+          ]),
+
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'midi-project-files'},
+                title: language.$('releaseInfo.midiProjectFiles.heading'),
+              }),
+
+            relations.midiProjectFilesList,
+          ]),
+
+          html.tags([
+            relations.contentHeading.clone()
+              .slots({
+                attributes: {id: 'additional-files'},
+                title: language.$('releaseInfo.additionalFiles.heading'),
+              }),
+
+            relations.additionalFilesList,
+          ]),
+
+          relations.artistCommentarySection,
+        ],
+
+        navLinkStyle: 'hierarchical',
+
+        navLinks: [
+          {auto: 'home'},
+
+          {html: relations.albumLink.slot('color', false)},
+
+          {
+            html:
+              language.encapsulate(pageCapsule, 'nav.track', capsule => {
+                const options = {};
+
+                options.track =
+                  relations.trackLink
+                    .slot('attributes', {class: 'current'});
+
+                if (data.hasTrackNumbers) {
+                  capsule += '.withNumber';
+                  options.number = data.trackNumber;
+                }
+
+                return language.$(capsule, options);
+              }),
+          },
+        ],
+
+        navBottomRowContent:
+          relations.albumNavAccent.slots({
+            showTrackNavigation: true,
+            showExtraLinks: false,
+          }),
+
+        navContent:
+          relations.chronologyLinks,
+
+        secondaryNav:
+          relations.secondaryNav
+            .slot('mode', 'track'),
+
+        leftSidebar: relations.sidebar,
+
+        socialEmbed: relations.socialEmbed,
+      })),
 };
 
 /*
diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js
index 4760ff2f..bc3b6035 100644
--- a/src/content/dependencies/generateTrackList.js
+++ b/src/content/dependencies/generateTrackList.js
@@ -31,14 +31,16 @@ export default {
         contributionLinks: relations.contributionLinks,
       }).map(({trackLink, contributionLinks}) =>
           html.tag('li',
-            (empty(contributionLinks)
-              ? trackLink
-              : language.$('trackList.item.withArtists', {
-                  track: trackLink,
-                  by:
+            language.encapsulate('trackList.item', itemCapsule =>
+              language.encapsulate(itemCapsule, workingCapsule => {
+                const workingOptions = {track: trackLink};
+
+                if (!empty(contributionLinks)) {
+                  workingCapsule += '.withArtists';
+                  workingOptions.by =
                     html.tag('span', {class: 'by'},
                       html.metatag('chunkwrap', {split: ','},
-                        language.$('trackList.item.withArtists.by', {
+                        language.$(itemCapsule, 'withArtists.by', {
                           artists:
                             language.formatConjunctionList(
                               contributionLinks.map(link =>
@@ -46,6 +48,9 @@ export default {
                                   showContribution: slots.showContribution,
                                   showIcons: slots.showIcons,
                                 }))),
-                        }))),
-                }))))),
+                        })));
+                }
+
+                return language.$(workingCapsule, workingOptions);
+              }))))),
 };
diff --git a/src/content/dependencies/generateTrackListDividedByGroups.js b/src/content/dependencies/generateTrackListDividedByGroups.js
index 21dc9ac1..3cba479e 100644
--- a/src/content/dependencies/generateTrackListDividedByGroups.js
+++ b/src/content/dependencies/generateTrackListDividedByGroups.js
@@ -80,53 +80,59 @@ export default {
 
   generate: (data, relations, slots, {html, language}) =>
     relations.flatList ??
-    html.tag('dl', {[html.onlyIfContent]: true}, [
-      stitchArrays({
-        groupName: data.groupNames,
-        groupLink: relations.groupLinks,
-        trackList: relations.groupedTrackLists,
-      }).map(({
-          groupName,
-          groupLink,
-          trackList,
-        }) => [
-          (slots.headingString
-            ? relations.contentHeading.clone().slots({
-                tag: 'dt',
-
-                title:
-                  language.$('trackList.fromGroup', {
-                    group: groupLink
-                  }),
-
-                stickyTitle:
-                  language.$(slots.headingString, 'sticky', 'fromGroup', {
-                    group: groupName,
-                  }),
-              })
-            : html.tag('dt',
-                language.$('trackList.fromGroup', {
-                  group: groupLink
-                }))),
-
-          html.tag('dd', trackList),
-        ]),
-
-      relations.ungroupedTrackList && [
-        (slots.headingString
-          ? relations.contentHeading.clone().slots({
-              tag: 'dt',
-
-              title:
-                language.$('trackList.fromOther'),
-
-              stickyTitle:
-                language.$(slots.headingString, 'sticky', 'fromOther'),
-            })
-          : html.tag('dt',
-              language.$('trackList.fromOther'))),
-
-        html.tag('dd', relations.ungroupedTrackList),
-      ],
-    ]),
+
+    html.tag('dl',
+      {[html.onlyIfContent]: true},
+
+      language.encapsulate('trackList', listCapsule => [
+        stitchArrays({
+          groupName: data.groupNames,
+          groupLink: relations.groupLinks,
+          trackList: relations.groupedTrackLists,
+        }).map(({
+            groupName,
+            groupLink,
+            trackList,
+          }) => [
+            language.encapsulate(listCapsule, 'fromGroup', capsule =>
+              (slots.headingString
+                ? relations.contentHeading.clone().slots({
+                    tag: 'dt',
+
+                    title:
+                      language.$(capsule, {
+                        group: groupLink
+                      }),
+
+                    stickyTitle:
+                      language.$(slots.headingString, 'sticky', 'fromGroup', {
+                        group: groupName,
+                      }),
+                  })
+                : html.tag('dt',
+                    language.$(capsule, {
+                      group: groupLink
+                    })))),
+
+            html.tag('dd', trackList),
+          ]),
+
+        relations.ungroupedTrackList && [
+          language.encapsulate(listCapsule, 'fromOther', capsule =>
+            (slots.headingString
+              ? relations.contentHeading.clone().slots({
+                  tag: 'dt',
+
+                  title:
+                    language.$(capsule),
+
+                  stickyTitle:
+                    language.$(slots.headingString, 'sticky', 'fromOther'),
+                })
+              : html.tag('dt',
+                  language.$(capsule)))),
+
+          html.tag('dd', relations.ungroupedTrackList),
+        ],
+      ])),
 };
diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js
index 88a4cdc7..e234dd5d 100644
--- a/src/content/dependencies/generateTrackReleaseInfo.js
+++ b/src/content/dependencies/generateTrackReleaseInfo.js
@@ -47,44 +47,47 @@ export default {
   },
 
   generate: (data, relations, {html, language}) =>
-    html.tags([
-      html.tag('p',
-        {[html.onlyIfContent]: true},
-        {[html.joinChildren]: html.tag('br')},
-
-        [
-          relations.artistContributionLinks
-            .slots({stringKey: 'releaseInfo.by'}),
-
-          relations.coverArtistContributionsLine
-            ?.slots({stringKey: 'releaseInfo.coverArtBy'}),
-
-          language.$('releaseInfo.released', {
-            [language.onlyIfOptions]: ['date'],
-            date: language.formatDate(data.date),
-          }),
-
-          language.$('releaseInfo.artReleased', {
-            [language.onlyIfOptions]: ['date'],
-            date: language.formatDate(data.coverArtDate),
-          }),
-
-          language.$('releaseInfo.duration', {
-            [language.onlyIfOptions]: ['duration'],
-            duration: language.formatDuration(data.duration),
-          }),
-        ]),
-
-      html.tag('p',
-        (relations.externalLinks
-          ? language.$('releaseInfo.listenOn', {
-              links:
-                language.formatDisjunctionList(
-                  relations.externalLinks
-                    .map(link => link.slot('context', 'track'))),
-            })
-          : language.$('releaseInfo.listenOn.noLinks', {
-              name: html.tag('i', data.name),
-            }))),
-    ]),
+    language.encapsulate('releaseInfo', capsule =>
+      html.tags([
+        html.tag('p',
+          {[html.onlyIfContent]: true},
+          {[html.joinChildren]: html.tag('br')},
+
+          [
+            relations.artistContributionLinks
+              .slots({stringKey: capsule + '.by'}),
+
+            relations.coverArtistContributionsLine
+              ?.slots({stringKey: capsule + '.coverArtBy'}),
+
+            language.$(capsule, 'released', {
+              [language.onlyIfOptions]: ['date'],
+              date: language.formatDate(data.date),
+            }),
+
+            language.$(capsule, 'artReleased', {
+              [language.onlyIfOptions]: ['date'],
+              date: language.formatDate(data.coverArtDate),
+            }),
+
+            language.$(capsule, 'duration', {
+              [language.onlyIfOptions]: ['duration'],
+              duration: language.formatDuration(data.duration),
+            }),
+          ]),
+
+        html.tag('p',
+          language.encapsulate(capsule, 'listenOn', capsule =>
+            (relations.externalLinks
+              ? language.$(capsule, {
+                  links:
+                    language.formatDisjunctionList(
+                      relations.externalLinks
+                        .map(link => link.slot('context', 'track'))),
+                })
+              : language.$(capsule, 'noLinks', {
+                  name:
+                    html.tag('i', data.name),
+                })))),
+      ])),
 };
diff --git a/src/content/dependencies/generateTrackSocialEmbed.js b/src/content/dependencies/generateTrackSocialEmbed.js
index 0337fc46..9868f0e2 100644
--- a/src/content/dependencies/generateTrackSocialEmbed.js
+++ b/src/content/dependencies/generateTrackSocialEmbed.js
@@ -39,35 +39,35 @@ export default {
     return data;
   },
 
-  generate(data, relations, {absoluteTo, language, urls}) {
-    return relations.socialEmbed.slots({
-      title:
-        language.$('trackPage.socialEmbed.title', {
-          track: data.trackName,
-        }),
+  generate: (data, relations, {absoluteTo, language, urls}) =>
+    language.encapsulate('trackPage.socialEmbed', embedCapsule =>
+      relations.socialEmbed.slots({
+        title:
+          language.$(embedCapsule, 'title', {
+            track: data.trackName,
+          }),
 
-      headingContent:
-        language.$('trackPage.socialEmbed.heading', {
-          album: data.albumName,
-        }),
+        headingContent:
+          language.$(embedCapsule, 'heading', {
+            album: data.albumName,
+          }),
 
-      headingLink:
-        absoluteTo('localized.album', data.albumDirectory),
+        headingLink:
+          absoluteTo('localized.album', data.albumDirectory),
 
-      imagePath:
-        (data.imageSource === 'album'
-          ? '/' +
-            urls
-              .from('shared.root')
-              .to('media.albumCover', data.albumDirectory, data.coverArtFileExtension)
-       : data.imageSource === 'track'
-          ? '/' +
-            urls
-              .from('shared.root')
-              .to('media.trackCover', data.albumDirectory, data.trackDirectory, data.coverArtFileExtension)
-          : null),
-    });
-  },
+        imagePath:
+          (data.imageSource === 'album'
+            ? '/' +
+              urls
+                .from('shared.root')
+                .to('media.albumCover', data.albumDirectory, data.coverArtFileExtension)
+         : data.imageSource === 'track'
+            ? '/' +
+              urls
+                .from('shared.root')
+                .to('media.trackCover', data.albumDirectory, data.trackDirectory, data.coverArtFileExtension)
+            : null),
+      })),
 };
 
 /*
diff --git a/src/content/dependencies/generateWikiHomeNewsBox.js b/src/content/dependencies/generateWikiHomeNewsBox.js
index bd0e4797..83a27695 100644
--- a/src/content/dependencies/generateWikiHomeNewsBox.js
+++ b/src/content/dependencies/generateWikiHomeNewsBox.js
@@ -1,4 +1,4 @@
-import {empty, stitchArrays} from '#sugar';
+import {stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
@@ -39,49 +39,48 @@ export default {
         .map(entry => entry.date),
   }),
 
-  generate(data, relations, {html, language}) {
-    if (empty(relations.entryContents)) {
-      return html.blank();
-    }
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('homepage.news', boxCapsule =>
+      relations.box.slots({
+        attributes: {class: 'latest-news-sidebar-box'},
+        collapsible: false,
 
-    return relations.box.slots({
-      attributes: {class: 'latest-news-sidebar-box'},
-      collapsible: false,
+        content: [
+          html.tag('h1',
+            {[html.onlyIfSiblings]: true},
+            language.$(boxCapsule, 'title')),
 
-      content: [
-        html.tag('h1', language.$('homepage.news.title')),
+          stitchArrays({
+            date: data.entryDates,
+            content: relations.entryContents,
+            mainLink: relations.entryMainLinks,
+            readMoreLink: relations.entryReadMoreLinks,
+          }).map(({
+              date,
+              content,
+              mainLink,
+              readMoreLink,
+            }, index) =>
+              language.encapsulate(boxCapsule, 'entry', entryCapsule =>
+                html.tag('article', {class: 'news-entry'},
+                  index === 0 &&
+                    {class: 'first-news-entry'},
 
-        stitchArrays({
-          date: data.entryDates,
-          content: relations.entryContents,
-          mainLink: relations.entryMainLinks,
-          readMoreLink: relations.entryReadMoreLinks,
-        }).map(({
-            date,
-            content,
-            mainLink,
-            readMoreLink,
-          }, index) =>
-            html.tag('article', {class: 'news-entry'},
-              index === 0 &&
-                {class: 'first-news-entry'},
+                  [
+                    html.tag('h2', [
+                      html.tag('time', language.formatDate(date)),
+                      mainLink,
+                    ]),
 
-              [
-                html.tag('h2', [
-                  html.tag('time', language.formatDate(date)),
-                  mainLink,
-                ]),
+                    content.slot('thumb', 'medium'),
 
-                content.slot('thumb', 'medium'),
-
-                html.tag('p',
-                  {[html.onlyIfContent]: true},
-                  readMoreLink
-                    ?.slots({
-                      content: language.$('homepage.news.entry.viewRest'),
-                    })),
-              ])),
-      ],
-    });
-  },
+                    html.tag('p',
+                      {[html.onlyIfContent]: true},
+                      readMoreLink
+                        ?.slots({
+                          content: language.$(entryCapsule, 'viewRest'),
+                        })),
+                  ]))),
+        ],
+      })),
 };
diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js
index ab2eca93..79bba441 100644
--- a/src/content/dependencies/listRandomPageLinks.js
+++ b/src/content/dependencies/listRandomPageLinks.js
@@ -74,20 +74,22 @@ export default {
   },
 
   generate(data, relations, {html, language}) {
+    const capsule = language.encapsulate('listingPage.other.randomPages');
+
     const miscellaneousChunkRows = [
-      {
+      language.encapsulate(capsule, 'chunk.item.randomArtist', capsule => ({
         stringsKey: 'randomArtist',
 
         mainLink:
           html.tag('a',
             {href: '#', 'data-random': 'artist'},
-            language.$('listingPage.other.randomPages.chunk.item.randomArtist.mainLink')),
+            language.$(capsule, 'mainLink')),
 
         atLeastTwoContributions:
           html.tag('a',
             {href: '#', 'data-random': 'artist-more-than-one-contrib'},
-            language.$('listingPage.other.randomPages.chunk.item.randomArtist.atLeastTwoContributions')),
-      },
+            language.$(capsule, 'atLeastTwoContributions')),
+      })),
 
       {stringsKey: 'randomAlbumWholeSite'},
       {stringsKey: 'randomTrackWholeSite'},
@@ -104,24 +106,25 @@ export default {
 
       content: [
         html.tag('p',
-          language.$('listingPage.other.randomPages.chooseLinkLine', {
-            fromPart:
-              (relations.groupLinks
-                ? language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.dividedByGroups')
-                : language.$('listingPage.other.randomPages.chooseLinkLine.fromPart.notDividedByGroups')),
+          language.encapsulate(capsule, 'chooseLinkLine', capsule =>
+            language.$(capsule, {
+              fromPart:
+                (relations.groupLinks
+                  ? language.$(capsule, 'fromPart.dividedByGroups')
+                  : language.$(capsule, 'fromPart.notDividedByGroups')),
 
-            browserSupportPart:
-              language.$('listingPage.other.randomPages.chooseLinkLine.browserSupportPart'),
-          })),
+              browserSupportPart:
+                language.$(capsule, 'browserSupportPart'),
+            }))),
 
         html.tag('p', {id: 'data-loading-line'},
-          language.$('listingPage.other.randomPages.dataLoadingLine')),
+          language.$(capsule, 'dataLoadingLine')),
 
         html.tag('p', {id: 'data-loaded-line'},
-          language.$('listingPage.other.randomPages.dataLoadedLine')),
+          language.$(capsule, 'dataLoadedLine')),
 
         html.tag('p', {id: 'data-error-line'},
-          language.$('listingPage.other.randomPages.dataErrorLine')),
+          language.$(capsule, 'dataErrorLine')),
       ],
 
       showSkipToSection: true,
@@ -148,17 +151,18 @@ export default {
 
         ...
           (relations.groupLinks
-            ? relations.groupLinks.map(() => ({
-                randomAlbum:
-                  html.tag('a',
-                    {href: '#', 'data-random': 'album-in-group-dl'},
-                    language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomAlbum')),
-
-                randomTrack:
-                  html.tag('a',
-                    {href: '#', 'data-random': 'track-in-group-dl'},
-                    language.$('listingPage.other.randomPages.chunk.title.fromGroup.accent.randomTrack')),
-              }))
+            ? relations.groupLinks.map(() =>
+                language.encapsulate(capsule, 'chunk.title.fromGroup.accent', capsule => ({
+                  randomAlbum:
+                    html.tag('a',
+                      {href: '#', 'data-random': 'album-in-group-dl'},
+                      language.$(capsule, 'randomAlbum')),
+
+                  randomTrack:
+                    html.tag('a',
+                      {href: '#', 'data-random': 'track-in-group-dl'},
+                      language.$(capsule, 'randomTrack')),
+                })))
             : [null]),
       ],
 
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index ee2c3938..5f803a3b 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -279,15 +279,16 @@ export default {
                       {class: 'align-center'},
 
                     {title:
-                      language.$('misc.external.opensInNewTab', {
-                        link:
-                          language.formatExternalLink(link, {
-                            style: 'platform',
-                          }),
-
-                        annotation:
-                          language.$('misc.external.opensInNewTab.annotation'),
-                      }).toString()},
+                      language.encapsulate('misc.external.opensInNewTab', capsule =>
+                        language.$(capsule, {
+                          link:
+                            language.formatExternalLink(link, {
+                              style: 'platform',
+                            }),
+
+                          annotation:
+                            language.$(capsule, 'annotation'),
+                        }).toString())},
 
                     content);
               }