@eox/pages-theme-eox 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ # [1.1.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.0.0...v1.1.0) (2026-02-16)
4
+
5
+ ### Bug Fixes
6
+
7
+ - **style:** css adjustments ([f2dc9a1](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/f2dc9a1db294b018e49c07acdb0f030bd98e03bf))
8
+
9
+ ### Features
10
+
11
+ - i18n support ([27af579](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/27af579611046e81baac7f49e4dd05db8e1cac12))
12
+ - introduce tutorial component ([158c7fd](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/158c7fdeb137f7b5598e0aa9819cf4793a5c8d81))
13
+
3
14
  # [1.0.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v0.11.5...v1.0.0) (2026-01-20)
4
15
 
5
16
  ### Features
package/README.md CHANGED
@@ -72,6 +72,45 @@ The following standard VitePress configurations are handled as follows:
72
72
  - **Footer**: Supported. The theme uses `theme.footer.copyright` for the copyright text.
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.0",
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",
@@ -49,4 +49,29 @@ describe("<Footer />", () => {
49
49
  );
50
50
  cy.contains("a", "Privacy").should("have.attr", "href", "/privacy");
51
51
  });
52
+
53
+ it("renders i18n overrides", () => {
54
+ __setMockData({
55
+ site: { title: "EOX Site" },
56
+ theme: {
57
+ logo: { light: "/logo.png" },
58
+ nav: [],
59
+ footer: {
60
+ copyright: "© 2026 EOX",
61
+ },
62
+ i18n: {
63
+ About: "Über uns",
64
+ Legal: "Rechtliches",
65
+ "Powered by": "Unterstützt von",
66
+ },
67
+ theme: {
68
+ brandConfig: {},
69
+ },
70
+ },
71
+ });
72
+ cy.mount(Footer);
73
+ cy.contains("Über uns").should("exist");
74
+ cy.contains("Rechtliches").should("exist");
75
+ cy.contains("Unterstützt von").should("exist");
76
+ });
52
77
  });
@@ -24,7 +24,7 @@
24
24
  >
25
25
  <p v-html="theme.footer.copyright"></p>
26
26
  <p class="middle-align">
27
- Powered by
27
+ {{ t("Powered by", theme.i18n) }}
28
28
  <a
29
29
  href="https://hub.eox.at"
30
30
  target="_blank"
@@ -41,7 +41,7 @@
41
41
  class="s6 l4"
42
42
  v-if="theme.nav.filter((i) => !i.action && i.link).length"
43
43
  >
44
- <p class="bold">About</p>
44
+ <p class="bold">{{ t("About", theme.i18n) }}</p>
45
45
  <p v-for="item in theme.nav.filter((i) => !i.action && i.link)">
46
46
  <a
47
47
  :href="withBase(item.link)"
@@ -65,7 +65,7 @@
65
65
  </p>
66
66
  </div>
67
67
  <div class="s6 l4">
68
- <p class="bold">Legal</p>
68
+ <p class="bold">{{ t("Legal", theme.i18n) }}</p>
69
69
  <p>
70
70
  <a
71
71
  :href="
@@ -74,7 +74,7 @@
74
74
  "
75
75
  target="_blank"
76
76
  class="link"
77
- >About</a
77
+ >{{ t("About", theme.i18n) }}</a
78
78
  >
79
79
  </p>
80
80
  <p>
@@ -85,7 +85,7 @@
85
85
  "
86
86
  target="_blank"
87
87
  class="link"
88
- >Terms & Conditions</a
88
+ >{{ t("Terms & Conditions", theme.i18n) }}</a
89
89
  >
90
90
  </p>
91
91
  <p>
@@ -96,11 +96,13 @@
96
96
  "
97
97
  target="_blank"
98
98
  class="link"
99
- >Privacy</a
99
+ >{{ t("Privacy", theme.i18n) }}</a
100
100
  >
101
101
  </p>
102
102
  <p v-if="theme.theme.brandConfig?.analytics">
103
- <a href="/cookie-settings" class="link">Cookie settings</a>
103
+ <a href="/cookie-settings" class="link">{{
104
+ t("Cookie settings", theme.i18n)
105
+ }}</a>
104
106
  </p>
105
107
  </div>
106
108
  </div>
@@ -113,7 +115,7 @@
113
115
 
114
116
  <script setup>
115
117
  import { useData, withBase } from "vitepress";
116
- import { trackEvent, getFlatList } from "../helpers";
118
+ import { trackEvent, getFlatList, t } from "../helpers";
117
119
  const { site, theme } = useData();
118
120
  </script>
119
121
 
@@ -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
  });