3

I want to create an astro component taking into account a variable in localstorage to get each component text, but Astro shows null.

let locale = "en"; //default
const userLanguage = localStorage.getItem("language");
if (userLanguage ) {
locale = userLanguage;
} let menu;
import(`../locale/${locale}/menu.json`).then((lang) =\> {
menu  = lang.default;
});

I need to find the way to have language based json or markup files and load each user's language. I was thinking about using svelte/react but I will have to create a lot of calls or maybe is there another way to make it?

Chris_xD
  • 31
  • 2

1 Answers1

5

Why it happens

As I understood you want to create *.astro component and use localStorage API within it. However, browser related API (such as document and window) is not accessible on the server i.e. in Astro and from MDN you can see that localStorage is part of window object.

The localStorage read-only property of the window interface allows you to access a Storage object for the Document's origin; the stored data is saved across browser sessions.

With that in mind the right usage of localStorage will be window.localStorage which will cause the following Astro error:

document (or window) is not defined

From Astro docs you can see what this actually means:

Astro components run on the server, so you can’t access these browser-specific objects within the frontmatter.

Potential solutions

So the potential solution will be to use Framework components with lifecycle hooks (e.g React's useEffect, Vue's onMounted and so on) or <script> as mentioned in Astro docs as well:

If the code is in an Astro component, move it to a <script> tag outside of the frontmatter. This tells Astro to run this code on the client, where document and window are available.

If the code is in a framework component, try to access these objects after rendering using lifecycle methods ... Tell the framework component to hydrate client-side by using a client: directive, like client:load, to run these lifecycle methods.

How would I solve it

Hovewer, from my experience I would move the async loading of json translation from the client to the server by just loading all the translations, i.e for each language.

Let's say you have the following folder structure for translations:

- locales
--- menu
----- en.json
----- ru.json
----- es.json
--- other_feature
----- en.json
----- ru.json
----- es.json  

Then we can use glob import to import everything at once:

const translations = import.meta.glob('./locales/menu/*.json', { eager: true, import: 'default' })

Then you just pass this translations object (which is object with keys representing path to file and values representing the json string) to your Framework component. You can learn more about glob import here.

Framework component itself should use lifecycle method to access the localStorage to read user locale and conditionally take the correct translation from the input props. Below the Vue example:

<script setup>
import { onMounted, ref } from 'vue'

const props = defineProps(['translations'])
const translation = ref({})

onMounted(() => {
  const userLocale = window.localeStorage.getItem("language")
  // take the correct translation from all translations
  translation.value = JSON.parse(
    translations[Object.keys(translations).find(key => key.includes(userLocale))]
  )
})
</script>

<template>
  <p>This message displayed in your mother tongue: {{ translation.message }}</p>
</template>

So the final Astro file can look like this:

---
const translations = import.meta.glob('./locales/menu/*.json', { eager: true, import: 'default' })
---

<div>
  <!-- Keep in mind that using `client:load` you might face hydration issues. They can be resolved by explicitly rendering the component on the client using `client:only` -->
  <VueMessageComponent translations={ translations } client:load />
</div>

I hope it helps but keep in mind that I wrote that in JavaScript (not in TypeScript) which can cause some issues with null/undefined values. Also, I did not test this code so it might not work just out of the box :)

e3stpavel
  • 61
  • 1
  • 4