@4verburga/alpine-spanishplus 2.0.0-dev.7ab3971 → 2.0.0-dev.838f140

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.
@@ -24,6 +24,9 @@ const alpine = useAppConfig().alpine
24
24
  <div v-if="alpine.socials && Object.entries(alpine.socials)" class="social">
25
25
  <SocialIcons :socials="alpine.socials" />
26
26
  </div>
27
+ <div class="language-switch">
28
+ <LanguageSwitcher />
29
+ </div>
27
30
  <div class="color-mode-switch">
28
31
  <ColorModeSwitch />
29
32
  </div>
@@ -7,7 +7,7 @@ const show = ref(false)
7
7
  <template>
8
8
  <header :class="alpine.header.position || 'left'">
9
9
  <div class="menu">
10
- <button @click="(show = !show)" aria-label="Navigation Menu">
10
+ <button @click="(show = !show)" :aria-label="$t('nav.menu')">
11
11
  <svg width="24" height="24" viewBox="0 0 68 68" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
12
12
  <path d="M8 34C8 32.1362 8 31.2044 8.30448 30.4693C8.71046 29.4892 9.48915 28.7105 10.4693 28.3045C11.2044 28 12.1362 28 14 28C15.8638 28 16.7956 28 17.5307 28.3045C18.5108 28.7105 19.2895 29.4892 19.6955 30.4693C20 31.2044 20 32.1362 20 34C20 35.8638 20 36.7956 19.6955 37.5307C19.2895 38.5108 18.5108 39.2895 17.5307 39.6955C16.7956 40 15.8638 40 14 40C12.1362 40 11.2044 40 10.4693 39.6955C9.48915 39.2895 8.71046 38.5108 8.30448 37.5307C8 36.7956 8 35.8638 8 34Z" />
13
13
  <path d="M28 34C28 32.1362 28 31.2044 28.3045 30.4693C28.7105 29.4892 29.4892 28.7105 30.4693 28.3045C31.2044 28 32.1362 28 34 28C35.8638 28 36.7956 28 37.5307 28.3045C38.5108 28.7105 39.2895 29.4892 39.6955 30.4693C40 31.2044 40 32.1362 40 34C40 35.8638 40 36.7956 39.6955 37.5307C39.2895 38.5108 38.5108 39.2895 37.5307 39.6955C36.7956 40 35.8638 40 34 40C32.1362 40 31.2044 40 30.4693 39.6955C29.4892 39.2895 28.7105 38.5108 28.3045 37.5307C28 36.7956 28 35.8638 28 34Z" />
@@ -10,19 +10,19 @@ const onClick = () => {
10
10
  </script>
11
11
 
12
12
  <template>
13
- <button aria-label="Color Mode" @click="onClick">
13
+ <button :aria-label="$t('colorMode.label')" @click="onClick">
14
14
  <ColorScheme>
15
15
  <template v-if="colorMode.preference === 'dark'">
16
16
  <Icon name="uil:moon" />
17
- <span class="sr-only">Dark mode</span>
17
+ <span class="sr-only">{{ $t('colorMode.dark') }}</span>
18
18
  </template>
19
19
  <template v-else-if="colorMode.preference === 'light'">
20
20
  <Icon name="uil:sun" />
21
- <span class="sr-only">Light mode</span>
21
+ <span class="sr-only">{{ $t('colorMode.light') }}</span>
22
22
  </template>
23
23
  <template v-else>
24
24
  <Icon name="uil:desktop" />
25
- <span class="sr-only">System mode</span>
25
+ <span class="sr-only">{{ $t('colorMode.system') }}</span>
26
26
  </template>
27
27
  </ColorScheme>
28
28
  </button>
@@ -1,15 +1,15 @@
1
1
  <template>
2
2
  <section>
3
3
  <p class="message">
4
- This page could not be found
4
+ {{ $t('notFound.message') }}
5
5
  </p>
6
6
 
7
7
  <p class="status">
8
- 404
8
+ {{ $t('notFound.code') }}
9
9
  </p>
10
10
 
11
11
  <NuxtLink to="/">
12
- Back to homepage
12
+ {{ $t('notFound.backHome') }}
13
13
  <Icon name="ph:arrow-right" />
14
14
  </NuxtLink>
15
15
  </section>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ const { locale, locales, setLocale } = useI18n()
3
+ const switchLocalePath = useSwitchLocalePath()
4
+
5
+ const availableLocales = computed(() =>
6
+ locales.value.filter((l: any) => l.code !== locale.value)
7
+ )
8
+ </script>
9
+
10
+ <template>
11
+ <div class="language-switcher">
12
+ <NuxtLink
13
+ v-for="loc in availableLocales"
14
+ :key="loc.code"
15
+ :to="switchLocalePath(loc.code)"
16
+ class="locale-link"
17
+ >
18
+ {{ loc.code.toUpperCase() }}
19
+ </NuxtLink>
20
+ </div>
21
+ </template>
22
+
23
+ <style scoped lang="ts">
24
+ css({
25
+ '.language-switcher': {
26
+ display: 'flex',
27
+ alignItems: 'center',
28
+ gap: '{space.2}',
29
+ '.locale-link': {
30
+ fontSize: '{text.sm.fontSize}',
31
+ fontWeight: '{fontWeight.medium}',
32
+ padding: '{space.1} {space.2}',
33
+ borderRadius: '{radii.md}',
34
+ '&:hover': {
35
+ color: '{color.primary.500}',
36
+ }
37
+ }
38
+ }
39
+ })
40
+ </style>
@@ -1,18 +1,28 @@
1
1
  <script lang="ts" setup>
2
2
  const { navigation } = useContent()
3
+ const { locale } = useI18n()
3
4
 
4
5
  const emits = defineEmits(['linkClick'])
5
6
 
6
7
  function handleClick() {
7
8
  emits('linkClick')
8
9
  }
10
+
11
+ const localizedNavigation = computed(() => {
12
+ if (!navigation.value) return []
13
+ // Find the locale subtree in navigation (e.g. /es or /en)
14
+ const localeRoot = navigation.value.find(
15
+ (item: any) => item._path === `/${locale.value}`
16
+ )
17
+ return localeRoot?.children || navigation.value
18
+ })
9
19
  </script>
10
20
 
11
21
  <template>
12
22
  <nav>
13
23
  <ul>
14
24
  <li
15
- v-for="link of navigation"
25
+ v-for="link of localizedNavigation"
16
26
  :key="link._path"
17
27
  >
18
28
  <NuxtLink
@@ -2,6 +2,8 @@
2
2
  import { withTrailingSlash } from 'ufo'
3
3
  import ArticleIndexEntry from './ArticleIndexEntry.vue';
4
4
 
5
+ const { locale } = useI18n()
6
+
5
7
  const props = defineProps({
6
8
  path: {
7
9
  type: String,
@@ -9,8 +11,10 @@ const props = defineProps({
9
11
  }
10
12
  })
11
13
 
14
+ const contentPath = computed(() => `${locale.value}/${props.path}`)
15
+
12
16
  // @ts-ignore
13
- const { data: _articles } = await useAsyncData(props.path, async () => await queryContent(withTrailingSlash(props.path)).sort({ date: -1 }).find())
17
+ const { data: _articles } = await useAsyncData(contentPath.value, async () => await queryContent(withTrailingSlash(contentPath.value)).sort({ date: -1 }).find())
14
18
 
15
19
  // create new fields year and month
16
20
  // const articles = computed(() => _articles.value || [])
@@ -5,6 +5,7 @@ import { useRouter, useRoute } from 'vue-router'
5
5
 
6
6
  const router = useRouter()
7
7
  const route = useRoute()
8
+ const { locale } = useI18n()
8
9
 
9
10
  const props = defineProps({
10
11
  path: {
@@ -14,10 +15,11 @@ const props = defineProps({
14
15
  })
15
16
 
16
17
  const currentYear = ref(parseInt(route.query.year as string) || new Date().getFullYear())
17
- const years = ref([2025, 2024, 2023]) // Add more years as needed
18
+ const startYear = 2023
19
+ const years = ref(Array.from({ length: new Date().getFullYear() - startYear + 1 }, (_, i) => new Date().getFullYear() - i))
18
20
 
19
21
  const fetchArticles = async (year: number) => {
20
- const path = `${props.path}/${year}`
22
+ const path = `${locale.value}/${props.path}/${year}`
21
23
  const { data } = await useAsyncData(path, async () => await queryContent(withTrailingSlash(path)).sort({ date: -1 }).find())
22
24
  return data
23
25
  }
@@ -70,7 +72,7 @@ const yearButtons = computed(() => {
70
72
  </div>
71
73
  </div>
72
74
  <div v-else class="tour">
73
- <p>Seems like there are no articles for {{ currentYear }}.</p>
75
+ <p>{{ $t('articles.empty', { year: currentYear }) }}</p>
74
76
  </div>
75
77
  <div class="spacing"> </div>
76
78
  <div class="navigation-buttons">
@@ -2,6 +2,7 @@
2
2
  import type { PropType } from 'vue'
3
3
  import type { Field } from '../../types/contact'
4
4
  const alpine = useAppConfig().alpine
5
+ const { t } = useI18n()
5
6
 
6
7
  const { FORMSPREE_URL } = useRuntimeConfig().public
7
8
 
@@ -11,56 +12,59 @@ if (!FORMSPREE_URL) {
11
12
 
12
13
  const status = ref()
13
14
 
15
+ const defaultFields = computed<Field[]>(() => [
16
+ {
17
+ type: 'text',
18
+ model: 'name',
19
+ name: t('form.fields.name.label'),
20
+ placeholder: t('form.fields.name.placeholder'),
21
+ required: true,
22
+ layout: 'default'
23
+ },
24
+ {
25
+ type: 'email',
26
+ model: 'email',
27
+ name: t('form.fields.email.label'),
28
+ placeholder: t('form.fields.email.placeholder'),
29
+ required: true,
30
+ layout: 'default'
31
+ },
32
+ {
33
+ type: 'text',
34
+ model: 'text',
35
+ name: t('form.fields.subject.label'),
36
+ required: false,
37
+ layout: 'default'
38
+ },
39
+ {
40
+ type: 'textarea',
41
+ model: 'message',
42
+ name: t('form.fields.message.label'),
43
+ placeholder: t('form.fields.message.placeholder'),
44
+ required: true,
45
+ layout: 'big'
46
+ }
47
+ ])
48
+
14
49
  const props = defineProps({
15
50
  submitButtonText: {
16
51
  type: String,
17
- default: 'Send message'
52
+ default: ''
18
53
  },
19
54
  fields: {
20
55
  type: Array as PropType<Field[]>,
21
- default: () => [
22
- {
23
- type: 'text',
24
- model: 'name',
25
- name: 'Name',
26
- placeholder: 'Your name',
27
- required: true,
28
- layout: 'default'
29
- },
30
- {
31
- type: 'email',
32
- model: 'email',
33
- name: 'Email',
34
- placeholder: 'Your email',
35
- required: true,
36
- layout: 'default'
37
- },
38
- {
39
- type: 'text',
40
- model: 'text',
41
- name: 'Subject',
42
- required: false,
43
- layout: 'default'
44
- },
45
- {
46
- type: 'textarea',
47
- model: 'message',
48
- name: 'Message',
49
- placeholder: 'Your message',
50
- required: true,
51
- layout: 'big'
52
- }
53
- ]
56
+ default: null
54
57
  }
55
58
  })
56
59
 
57
- const form = reactive(props.fields.map(v => ({ ...v, data: '' })))
60
+ const activeFields = computed(() => props.fields || defaultFields.value)
61
+ const form = reactive(activeFields.value.map(v => ({ ...v, data: '' })))
58
62
 
59
63
  const onSend = async (e: any) => {
60
64
  e.preventDefault()
61
65
  const data = new FormData(e.target)
62
66
 
63
- status.value = 'Sending...'
67
+ status.value = t('form.sending')
64
68
 
65
69
  fetch(e.target.action, {
66
70
  method: e.target.method,
@@ -101,7 +105,7 @@ const onSend = async (e: any) => {
101
105
  </div>
102
106
  <div>
103
107
  <Button type="submit" :disabled="!FORMSPREE_URL">
104
- {{ status ? status : submitButtonText }}
108
+ {{ status ? status : (submitButtonText || $t('form.submitButton')) }}
105
109
  </Button>
106
110
  </div>
107
111
  </form>
@@ -1,5 +1,6 @@
1
1
  export const formatDate = (date: string) => {
2
- return new Date(date).toLocaleDateString('es', {
2
+ const { locale } = useI18n()
3
+ return new Date(date).toLocaleDateString(locale.value, {
3
4
  year: 'numeric',
4
5
  month: 'long',
5
6
  day: 'numeric'
@@ -0,0 +1,55 @@
1
+ {
2
+ "colorMode": {
3
+ "label": "Color Mode",
4
+ "dark": "Dark mode",
5
+ "light": "Light mode",
6
+ "system": "System mode"
7
+ },
8
+ "nav": {
9
+ "menu": "Navigation Menu"
10
+ },
11
+ "notFound": {
12
+ "message": "This page could not be found",
13
+ "code": "404",
14
+ "backHome": "Back to homepage"
15
+ },
16
+ "article": {
17
+ "back": "Back",
18
+ "byAuthor": "By",
19
+ "backToTop": "Back to top"
20
+ },
21
+ "articles": {
22
+ "empty": "Seems like there are no articles for {year}."
23
+ },
24
+ "hero": {
25
+ "title": "Hero title",
26
+ "description": "Hero description"
27
+ },
28
+ "form": {
29
+ "submitButton": "Send message",
30
+ "sending": "Sending...",
31
+ "fields": {
32
+ "name": {
33
+ "label": "Name",
34
+ "placeholder": "Your name"
35
+ },
36
+ "email": {
37
+ "label": "Email",
38
+ "placeholder": "Your email"
39
+ },
40
+ "subject": {
41
+ "label": "Subject"
42
+ },
43
+ "message": {
44
+ "label": "Message",
45
+ "placeholder": "Your message"
46
+ }
47
+ }
48
+ },
49
+ "footer": {
50
+ "message": "Follow me on"
51
+ },
52
+ "app": {
53
+ "description": "The minimalist blog theme"
54
+ }
55
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "colorMode": {
3
+ "label": "Modo de Color",
4
+ "dark": "Modo oscuro",
5
+ "light": "Modo claro",
6
+ "system": "Modo del sistema"
7
+ },
8
+ "nav": {
9
+ "menu": "Menú de Navegación"
10
+ },
11
+ "notFound": {
12
+ "message": "Esta página no pudo ser encontrada",
13
+ "code": "404",
14
+ "backHome": "Volver al inicio"
15
+ },
16
+ "article": {
17
+ "back": "Volver",
18
+ "byAuthor": "Por",
19
+ "backToTop": "Volver arriba"
20
+ },
21
+ "articles": {
22
+ "empty": "Parece que no hay artículos para {year}."
23
+ },
24
+ "hero": {
25
+ "title": "Título del héroe",
26
+ "description": "Descripción del héroe"
27
+ },
28
+ "form": {
29
+ "submitButton": "Enviar mensaje",
30
+ "sending": "Enviando...",
31
+ "fields": {
32
+ "name": {
33
+ "label": "Nombre",
34
+ "placeholder": "Tu nombre"
35
+ },
36
+ "email": {
37
+ "label": "Correo",
38
+ "placeholder": "Tu correo"
39
+ },
40
+ "subject": {
41
+ "label": "Asunto"
42
+ },
43
+ "message": {
44
+ "label": "Mensaje",
45
+ "placeholder": "Tu mensaje"
46
+ }
47
+ }
48
+ },
49
+ "footer": {
50
+ "message": "Sígueme en"
51
+ },
52
+ "app": {
53
+ "description": "El tema minimalista para blogs"
54
+ }
55
+ }
@@ -7,7 +7,7 @@
7
7
  >
8
8
  <Icon name="ph:arrow-left" />
9
9
  <span>
10
- Back
10
+ {{ $t('article.back') }}
11
11
  </span>
12
12
  </NuxtLink>
13
13
  <header>
@@ -24,7 +24,7 @@
24
24
  {{ formatDate(page.date) }}
25
25
  </time>
26
26
  <span v-if="page?.author?.name" class="author">
27
- &nbsp;•&nbsp;Por <strong>{{ page.author.name }}</strong>
27
+ &nbsp;•&nbsp;{{ $t('article.byAuthor') }} <strong>{{ page.author.name }}</strong>
28
28
  </span>
29
29
  </header>
30
30
 
@@ -35,7 +35,7 @@
35
35
  class="back-to-top"
36
36
  >
37
37
  <ProseA @click.prevent.stop="onBackToTop">
38
- {{ alpine?.backToTop?.text || 'Back to top' }}
38
+ {{ alpine?.backToTop?.text || $t('article.backToTop') }}
39
39
  <Icon :name="alpine?.backToTop?.icon || 'material-symbols:arrow-upward'" />
40
40
  </ProseA>
41
41
  </div>
package/nuxt.config.ts CHANGED
@@ -29,13 +29,7 @@ const updateModule = defineNuxtModule({
29
29
 
30
30
  // https://v3.nuxtjs.org/api/configuration/nuxt.config
31
31
  export default defineNuxtConfig({
32
- app: {
33
- head: {
34
- htmlAttrs: {
35
- lang: 'en'
36
- }
37
- }
38
- },
32
+ app: {},
39
33
  extends: [envModules.typography, envModules.elements],
40
34
  runtimeConfig: {
41
35
  public: {
@@ -47,8 +41,23 @@ export default defineNuxtConfig({
47
41
  envModules.tokens,
48
42
  envModules.studio,
49
43
  '@nuxt/content',
44
+ '@nuxtjs/i18n',
50
45
  updateModule as any
51
46
  ],
47
+ i18n: {
48
+ locales: [
49
+ { code: 'es', language: 'es-PE', name: 'Español', file: 'es.json' },
50
+ { code: 'en', language: 'en-US', name: 'English', file: 'en.json' }
51
+ ],
52
+ defaultLocale: 'es',
53
+ strategy: 'prefix',
54
+ langDir: 'i18n/locales',
55
+ detectBrowserLanguage: {
56
+ useCookie: true,
57
+ cookieKey: 'i18n_locale',
58
+ redirectOn: 'root'
59
+ }
60
+ },
52
61
  components: [
53
62
  { path: resolve('./components'), global: true },
54
63
  { path: resolve('./components/content'), global: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4verburga/alpine-spanishplus",
3
- "version": "2.0.0-dev.7ab3971",
3
+ "version": "2.0.0-dev.838f140",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,6 +12,7 @@
12
12
  "assets",
13
13
  "components",
14
14
  "composables",
15
+ "i18n",
15
16
  "layouts",
16
17
  "types",
17
18
  "app.config.ts",
@@ -35,6 +36,7 @@
35
36
  "@nuxt-themes/typography": "^0.11.0",
36
37
  "@nuxt/content": "^2.13.0",
37
38
  "@nuxthq/studio": "^0.14.1",
39
+ "@nuxtjs/i18n": "^8.5.5",
38
40
  "@vueuse/core": "^10.7.2",
39
41
  "ufo": "^1.5.4"
40
42
  },