@eox/pages-theme-eox 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.1](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.1.0...v1.1.1) (2026-03-02)
4
+
5
+ ### Bug Fixes
6
+
7
+ - render (HTML) footer message correctly ([6ea74ae](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/6ea74ae9df24fda7b677d6daa1e740b1d1dd6d97))
8
+
9
+ # [1.1.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.0.0...v1.1.0) (2026-02-16)
10
+
11
+ ### Bug Fixes
12
+
13
+ - **style:** css adjustments ([f2dc9a1](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/f2dc9a1db294b018e49c07acdb0f030bd98e03bf))
14
+
15
+ ### Features
16
+
17
+ - i18n support ([27af579](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/27af579611046e81baac7f49e4dd05db8e1cac12))
18
+ - introduce tutorial component ([158c7fd](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/158c7fdeb137f7b5598e0aa9819cf4793a5c8d81))
19
+
3
20
  # [1.0.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v0.11.5...v1.0.0) (2026-01-20)
4
21
 
5
22
  ### Features
package/README.md CHANGED
@@ -69,9 +69,48 @@ The following standard VitePress configurations are handled as follows:
69
69
  - **Site Title**: Explicitly disabled (`siteTitle: false`). The theme relies on the logo for branding.
70
70
  - **Logo**: Used for branding. Must be configured as an object `{ light: string, dark: string }`.
71
71
  - **Nav**: Fully supported (see above).
72
- - **Footer**: Supported. The theme uses `theme.footer.copyright` for the copyright text.
72
+ - **Footer**: Supported. The theme uses `theme.footer.message` for the footer message and `theme.footer.copyright` for the copyright text (both supporting HTML).
73
73
  - **Dark Mode**: The theme enforces a specific appearance. `appearance: false` is set in the base config.
74
74
 
75
+ ### Internationalization (i18n)
76
+
77
+ This theme leverages VitePress's native [i18n capabilities](https://vitepress.dev/guide/i18n) and extends them with a simplified configuration via `brandConfig`.
78
+
79
+ #### Multi-language Routing
80
+
81
+ You can define multiple locales in your `brandConfig`. The theme will automatically generate the corresponding VitePress routes and display a language switcher in the navigation bar.
82
+
83
+ ```javascript
84
+ // brandConfig
85
+ {
86
+ i18n: {
87
+ locale: {
88
+ en: {
89
+ "Read more about": "Read more about",
90
+ "Contact us": "Contact us"
91
+ },
92
+ de: {
93
+ "Read more about": "Mehr lesen über",
94
+ "Contact us": "Kontaktieren Sie uns"
95
+ }
96
+ },
97
+ currentLocale: "en" // This language will be at the root URL (/)
98
+ }
99
+ }
100
+ ```
101
+
102
+ #### Translation Pattern
103
+
104
+ The theme uses a "string-as-key" pattern. Most hardcoded UI elements (buttons, footer labels, etc.) use their English text as a lookup key. If a translation is provided in the `brandConfig.i18n.locale` object, it will be used; otherwise, it falls back to the original English text.
105
+
106
+ Nested keys are also supported via dot-notation if needed (e.g., `legal.privacy`).
107
+
108
+ #### Language Switcher
109
+
110
+ - **Desktop**: Appears as a translate icon in the top navigation bar.
111
+ - **Mobile**: Appears as a dedicated section within the mobile menu.
112
+ - The current language is automatically detected based on the URL path (e.g., `/de/` for German).
113
+
75
114
  **Note on Documentation Features**:
76
115
  This theme is primarily designed for landing pages and product showcases.
77
116
 
@@ -33,6 +33,20 @@ window.__helpersMock = {
33
33
  });
34
34
  return list;
35
35
  },
36
+ t: (key, i18n) => {
37
+ if (!i18n) return key;
38
+ const keys = key.split(".");
39
+ let result = i18n;
40
+ for (const k of keys) {
41
+ if (result && Object.prototype.hasOwnProperty.call(result, k)) {
42
+ result = result[k];
43
+ } else {
44
+ result = null;
45
+ break;
46
+ }
47
+ }
48
+ return result ? result : key;
49
+ },
36
50
  };
37
51
 
38
52
  export const trackEvent = (...args) => window.__helpersMock.trackEvent(...args);
@@ -46,3 +60,4 @@ export const showBanner = (...args) => window.__helpersMock.showBanner(...args);
46
60
  export const isActive = (...args) => window.__helpersMock.isActive(...args);
47
61
  export const getFlatList = (...args) =>
48
62
  window.__helpersMock.getFlatList(...args);
63
+ export const t = (...args) => window.__helpersMock.t(...args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eox/pages-theme-eox",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "Vitepress Theme with EOX branding",
6
6
  "main": "src/index.js",
@@ -3,21 +3,33 @@
3
3
  class="cookie-banner card surface medium-margin medium-padding medium-elevate no-round"
4
4
  >
5
5
  <p class="small-text">
6
- We use optional cookies to improve your experience and for marketing. Read
7
- our
6
+ {{
7
+ t(
8
+ "We use optional cookies to improve your experience and for marketing. Read our",
9
+ theme.i18n,
10
+ )
11
+ }}
8
12
  <a
9
13
  :href="theme.theme.brandConfig?.legal?.privacyPolicy"
10
14
  target="_blank"
11
15
  class="link"
12
16
  >
13
- privacy policy
17
+ {{ t("privacy policy", theme.i18n) }}
14
18
  </a>
15
- or <a class="link" href="/cookie-settings">manage cookies</a>.
19
+ {{ t("or", theme.i18n) }}
20
+ <a class="link" href="/cookie-settings">{{
21
+ t("manage cookies", theme.i18n)
22
+ }}</a
23
+ >.
16
24
  </p>
17
25
  <nav>
18
26
  <div class="max"></div>
19
- <button class="small" @click="accept">Accept all</button>
20
- <button class="small" @click="decline">Reject all</button>
27
+ <button class="small" @click="accept">
28
+ {{ t("Accept all", theme.i18n) }}
29
+ </button>
30
+ <button class="small" @click="decline">
31
+ {{ t("Reject all", theme.i18n) }}
32
+ </button>
21
33
  </nav>
22
34
  </div>
23
35
  </template>
@@ -30,6 +42,7 @@ import {
30
42
  declineCookies,
31
43
  enableTracking,
32
44
  showBanner,
45
+ t,
33
46
  } from "../helpers";
34
47
 
35
48
  const { theme } = useData();
@@ -1,11 +1,13 @@
1
1
  <template>
2
2
  <div class="VPPage">
3
- <h1>Cookie Settings</h1>
3
+ <h1>{{ t("Cookie Settings", theme.i18n) }}</h1>
4
4
  <p>
5
- We use cookies and similar technologies to improve your experience and for
6
- marketing purposes. Review and manage your cookie settings below to
7
- control your privacy. For more information on how we use cookies, please
8
- see our
5
+ {{
6
+ t(
7
+ "We use cookies and similar technologies to improve your experience and for marketing purposes. Review and manage your cookie settings below to control your privacy. For more information on how we use cookies, please see our",
8
+ theme.i18n,
9
+ )
10
+ }}
9
11
  <a
10
12
  class="link"
11
13
  :href="
@@ -13,7 +15,7 @@
13
15
  'https://eox.at/privacy-notice/'
14
16
  "
15
17
  target="_blank"
16
- >Privacy Policy</a
18
+ >{{ t("Privacy Policy", theme.i18n) }}</a
17
19
  >.
18
20
  </p>
19
21
  <div
@@ -40,17 +42,19 @@
40
42
  <details>
41
43
  <summary class="middle-align">
42
44
  <p class="primary-text bold">
43
- <span class="view">View</span><span class="hide">Hide</span> cookies
45
+ <span class="view">{{ t("View", theme.i18n) }}</span
46
+ ><span class="hide">{{ t("Hide", theme.i18n) }}</span>
47
+ {{ t("cookies", theme.i18n) }}
44
48
  </p>
45
49
  <i class="small mdi mdi-chevron-down"></i>
46
50
  </summary>
47
51
  <table>
48
52
  <thead>
49
53
  <tr>
50
- <th class="min">Name</th>
51
- <th>Domain</th>
52
- <th>Type</th>
53
- <th>Duration</th>
54
+ <th class="min">{{ t("Name", theme.i18n) }}</th>
55
+ <th>{{ t("Domain", theme.i18n) }}</th>
56
+ <th>{{ t("Type", theme.i18n) }}</th>
57
+ <th>{{ t("Duration", theme.i18n) }}</th>
54
58
  </tr>
55
59
  </thead>
56
60
  <tbody>
@@ -59,8 +63,8 @@
59
63
  <code>{{ cookie.Name }}</code>
60
64
  </td>
61
65
  <td>{{ cookie.Domain }}</td>
62
- <td>{{ cookie.Type }}</td>
63
- <td>{{ cookie.Duration }}</td>
66
+ <td>{{ t(cookie.Type, theme.i18n) }}</td>
67
+ <td>{{ t(cookie.Duration, theme.i18n) }}</td>
64
68
  </tr>
65
69
  </tbody>
66
70
  </table>
@@ -101,11 +105,11 @@ details[open] summary .hide {
101
105
  <script setup>
102
106
  import { onMounted } from "vue";
103
107
  import { useData, useRouter } from "vitepress";
104
- import { enableTracking, showBanner } from "../helpers.js";
108
+ import { enableTracking, showBanner, t } from "../helpers.js";
105
109
  const { theme } = useData();
106
110
  const router = useRouter();
107
111
 
108
- router.route.data.title = "Cookie Settings";
112
+ router.route.data.title = t("Cookie Settings", theme.value.i18n);
109
113
  router.onBeforeRouteChange = (to) => {
110
114
  if (to === "/cookie-settings") {
111
115
  showBanner(false);
@@ -118,40 +122,44 @@ router.onBeforeRouteChange = (to) => {
118
122
  };
119
123
 
120
124
  const cookies = {
121
- Essential: {
122
- description:
125
+ [t("Essential", theme.value.i18n)]: {
126
+ description: t(
123
127
  "Cookies that are strictly necessary for basic website or app functionality.",
128
+ theme.value.i18n,
129
+ ),
124
130
  required: true,
125
131
  cookies: [
126
132
  {
127
133
  Name: "mtm_consent_removed",
128
134
  Domain: `.${window.location.host}`,
129
- Type: "Opt-out management",
130
- Duration: "13 months",
135
+ Type: t("Opt-out management", theme.value.i18n),
136
+ Duration: t("13 months", theme.value.i18n),
131
137
  },
132
138
  {
133
139
  Name: "mtm_consent",
134
140
  Domain: `.${window.location.host}`,
135
- Type: "Consent management",
136
- Duration: "13 months",
141
+ Type: t("Consent management", theme.value.i18n),
142
+ Duration: t("13 months", theme.value.i18n),
137
143
  },
138
144
  ],
139
145
  },
140
- Analytics: {
141
- description:
146
+ [t("Analytics", theme.value.i18n)]: {
147
+ description: t(
142
148
  "Cookies that are required for analyzing website or app usage.",
149
+ theme.value.i18n,
150
+ ),
143
151
  cookies: [
144
152
  {
145
153
  Name: "_pk_id",
146
154
  Domain: `.${window.location.host}`,
147
- Type: "First-party website analytics",
148
- Duration: "13 months",
155
+ Type: t("First-party website analytics", theme.value.i18n),
156
+ Duration: t("13 months", theme.value.i18n),
149
157
  },
150
158
  {
151
159
  Name: "_pk_ses",
152
160
  Domain: `.${window.location.host}`,
153
- Type: "First-party website analytics",
154
- Duration: "30 minutes",
161
+ Type: t("First-party website analytics", theme.value.i18n),
162
+ Duration: t("30 minutes", theme.value.i18n),
155
163
  },
156
164
  ],
157
165
  },
@@ -1,5 +1,9 @@
1
1
  <script setup>
2
2
  import { ref } from "vue";
3
+ import { useData } from "vitepress";
4
+ import { t } from "../helpers";
5
+
6
+ const { theme } = useData();
3
7
 
4
8
  /**
5
9
  * @typedef {Object} TableRowData
@@ -51,7 +55,7 @@ const toggleRow = (index) => {
51
55
  <thead>
52
56
  <tr>
53
57
  <th v-for="header in headers" :key="header">
54
- {{ header }}
58
+ {{ t(header, theme.i18n) }}
55
59
  </th>
56
60
  </tr>
57
61
  </thead>
@@ -25,7 +25,9 @@
25
25
  :class="`button primary medium-elevate no-margin responsive-mobile`"
26
26
  style="margin-right: 12px !important"
27
27
  >
28
- <span>{{ primaryButton || `Read more about ${title}` }}</span>
28
+ <span>{{
29
+ primaryButton || `${t("Read more about", theme.i18n)} ${title}`
30
+ }}</span>
29
31
  <i class="mdi mdi-arrow-right"></i>
30
32
  </a>
31
33
  <a
@@ -35,7 +37,7 @@
35
37
  class="button secondary medium-elevate no-margin responsive-mobile"
36
38
  style="color: var(--on-surface); margin-top: 12px !important"
37
39
  >
38
- <span>{{ secondaryButton || "Contact sales" }}</span>
40
+ <span>{{ secondaryButton || t("Contact sales", theme.i18n) }}</span>
39
41
  <i class="mdi mdi-arrow-right"></i>
40
42
  </a>
41
43
  <a
@@ -45,7 +47,7 @@
45
47
  class="button border no-margin responsive-mobile"
46
48
  style="color: var(--on-surface); margin-top: 12px !important"
47
49
  >
48
- <span>{{ altButton || "Contact sales" }}</span>
50
+ <span>{{ altButton || t("Contact sales", theme.i18n) }}</span>
49
51
  </a>
50
52
  </div>
51
53
  </div>
@@ -68,7 +70,9 @@
68
70
  </template>
69
71
 
70
72
  <script setup>
71
- import { withBase } from "vitepress";
73
+ import { withBase, useData } from "vitepress";
74
+ import { t } from "../helpers.js";
75
+ const { theme } = useData();
72
76
  const props = defineProps([
73
77
  "dark",
74
78
  "icon",
@@ -12,6 +12,7 @@ describe("<Footer />", () => {
12
12
  { text: "Contact", link: "/contact" },
13
13
  ],
14
14
  footer: {
15
+ message: 'Powered by <a href="https://eox.at">EOX</a>',
15
16
  copyright: "© 2026 EOX",
16
17
  },
17
18
  theme: {
@@ -49,4 +50,61 @@ describe("<Footer />", () => {
49
50
  );
50
51
  cy.contains("a", "Privacy").should("have.attr", "href", "/privacy");
51
52
  });
53
+
54
+ it("renders footer message with HTML content", () => {
55
+ __setMockData({
56
+ site: { title: "EOX Site" },
57
+ theme: {
58
+ logo: { light: "/logo.png" },
59
+ nav: [
60
+ { text: "Link 1", link: "/link1" },
61
+ { text: "Contact", link: "/contact" },
62
+ ],
63
+ footer: {
64
+ message: 'Powered by <a href="https://eox.at">EOX</a>',
65
+ copyright: "© 2026 EOX",
66
+ },
67
+ theme: {
68
+ brandConfig: {},
69
+ },
70
+ },
71
+ });
72
+ cy.mount(Footer);
73
+ cy.contains("p", "Powered by")
74
+ .find("a")
75
+ .should("have.attr", "href", "https://eox.at")
76
+ .and("contain", "EOX");
77
+ });
78
+
79
+ it("renders i18n overrides", () => {
80
+ __setMockData({
81
+ site: { title: "EOX Site" },
82
+ theme: {
83
+ logo: { light: "/logo.png" },
84
+ nav: [],
85
+ footer: {
86
+ copyright: "© 2026 EOX",
87
+ },
88
+ i18n: {
89
+ About: "Über uns",
90
+ Legal: "Rechtliches",
91
+ "Powered by": "Unterstützt von",
92
+ },
93
+ theme: {
94
+ brandConfig: {},
95
+ },
96
+ },
97
+ });
98
+ cy.mount(Footer);
99
+ cy.contains("Über uns").should("exist");
100
+ cy.contains("Rechtliches").should("exist");
101
+ });
102
+
103
+ it("renders footer message with HTML content", () => {
104
+ cy.mount(Footer);
105
+ cy.contains("p", "Powered by")
106
+ .find("a")
107
+ .should("have.attr", "href", "https://eox.at")
108
+ .and("contain", "EOX");
109
+ });
52
110
  });
@@ -22,18 +22,8 @@
22
22
  theme.nav.find((i) => i.link && i.link.includes("contact")).text
23
23
  }}</a
24
24
  >
25
+ <p v-html="theme.footer.message"></p>
25
26
  <p v-html="theme.footer.copyright"></p>
26
- <p class="middle-align">
27
- Powered by
28
- <a
29
- href="https://hub.eox.at"
30
- target="_blank"
31
- class="left-margin small-margin"
32
- ><img
33
- src="https://hub.eox.at/hub/custom/logos/eoxhub.svg"
34
- style="height: 25px"
35
- /></a>
36
- </p>
37
27
  </div>
38
28
  <div class="s12 l6">
39
29
  <div class="grid large-line">
@@ -41,7 +31,7 @@
41
31
  class="s6 l4"
42
32
  v-if="theme.nav.filter((i) => !i.action && i.link).length"
43
33
  >
44
- <p class="bold">About</p>
34
+ <p class="bold">{{ t("About", theme.i18n) }}</p>
45
35
  <p v-for="item in theme.nav.filter((i) => !i.action && i.link)">
46
36
  <a
47
37
  :href="withBase(item.link)"
@@ -65,7 +55,7 @@
65
55
  </p>
66
56
  </div>
67
57
  <div class="s6 l4">
68
- <p class="bold">Legal</p>
58
+ <p class="bold">{{ t("Legal", theme.i18n) }}</p>
69
59
  <p>
70
60
  <a
71
61
  :href="
@@ -74,7 +64,7 @@
74
64
  "
75
65
  target="_blank"
76
66
  class="link"
77
- >About</a
67
+ >{{ t("About", theme.i18n) }}</a
78
68
  >
79
69
  </p>
80
70
  <p>
@@ -85,7 +75,7 @@
85
75
  "
86
76
  target="_blank"
87
77
  class="link"
88
- >Terms & Conditions</a
78
+ >{{ t("Terms & Conditions", theme.i18n) }}</a
89
79
  >
90
80
  </p>
91
81
  <p>
@@ -96,11 +86,13 @@
96
86
  "
97
87
  target="_blank"
98
88
  class="link"
99
- >Privacy</a
89
+ >{{ t("Privacy", theme.i18n) }}</a
100
90
  >
101
91
  </p>
102
92
  <p v-if="theme.theme.brandConfig?.analytics">
103
- <a href="/cookie-settings" class="link">Cookie settings</a>
93
+ <a href="/cookie-settings" class="link">{{
94
+ t("Cookie settings", theme.i18n)
95
+ }}</a>
104
96
  </p>
105
97
  </div>
106
98
  </div>
@@ -113,7 +105,7 @@
113
105
 
114
106
  <script setup>
115
107
  import { useData, withBase } from "vitepress";
116
- import { trackEvent, getFlatList } from "../helpers";
108
+ import { trackEvent, getFlatList, t } from "../helpers";
117
109
  const { site, theme } = useData();
118
110
  </script>
119
111
 
@@ -4,7 +4,7 @@
4
4
  <details>
5
5
  <summary>
6
6
  <span class="max" :class="{ active: isActive(item, route.path) }">{{
7
- item.text
7
+ t(item.text, theme.i18n)
8
8
  }}</span>
9
9
  <i class="mdi mdi-chevron-down"></i>
10
10
  </summary>
@@ -23,18 +23,18 @@
23
23
  data-ui="#mobile-menu"
24
24
  :class="{ active: isActive(item, route.path) }"
25
25
  >
26
- <span>{{ item.text }}</span>
26
+ <span>{{ t(item.text, theme.i18n) }}</span>
27
27
  </a>
28
28
  <span v-else :class="{ active: isActive(item, route.path) }">{{
29
- item.text
29
+ t(item.text, theme.i18n)
30
30
  }}</span>
31
31
  </li>
32
32
  </template>
33
33
  </template>
34
34
 
35
35
  <script setup>
36
- import { withBase, useRoute } from "vitepress";
37
- import { isActive } from "../helpers";
36
+ import { withBase, useRoute, useData } from "vitepress";
37
+ import { isActive, t } from "../helpers";
38
38
 
39
39
  defineProps({
40
40
  items: {
@@ -44,4 +44,5 @@ defineProps({
44
44
  });
45
45
 
46
46
  const route = useRoute();
47
+ const { theme } = useData();
47
48
  </script>
@@ -103,4 +103,41 @@ describe("<NavBar />", () => {
103
103
  cy.get("dialog#mobile-menu .social-links").should("exist");
104
104
  cy.get("dialog#mobile-menu .social-links a").should("have.length", 2);
105
105
  });
106
+
107
+ it("renders language switcher when multiple locales are defined", () => {
108
+ __setMockData({
109
+ site: {
110
+ title: "EOX",
111
+ locales: {
112
+ root: { label: "English", lang: "en" },
113
+ de: { label: "German", lang: "de" },
114
+ },
115
+ localeIndex: "root",
116
+ },
117
+ theme: {
118
+ logo: { light: "/logo.png" },
119
+ nav: [],
120
+ },
121
+ });
122
+
123
+ cy.viewport(1920, 1080);
124
+ cy.mount(NavBar);
125
+
126
+ // Desktop
127
+ cy.get(".nav-desktop .mdi-translate").should("exist");
128
+ cy.get(".nav-desktop menu li").should("have.length", 2);
129
+ cy.get(".nav-desktop menu li").contains("English").should("exist");
130
+ cy.get(".nav-desktop menu li").contains("German").should("exist");
131
+ cy.get(".nav-desktop menu li.active").contains("English").should("exist");
132
+
133
+ // Mobile
134
+ cy.viewport(375, 667);
135
+ cy.get("#mobile-menu details summary").contains("English").should("exist");
136
+ cy.get("#mobile-menu details summary .mdi-translate").should("exist");
137
+ cy.get("#mobile-menu details ul li").should("have.length", 1);
138
+ cy.get("#mobile-menu details ul li").contains("German").should("exist");
139
+ cy.get("#mobile-menu details ul li")
140
+ .contains("English")
141
+ .should("not.exist");
142
+ });
106
143
  });