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 :)