How to Build a Weather Forecast App with React, TypeScript, Tolgee and OpenWeather API

Introduction

Weather apps are a great way to practice building real-world applications, and so in the spirit of Hacktoberfest and with the power of Vite, TypeScript, and Tolgee, we will create a beautiful and multilingual weather app. Vite provides us with a lightning-fast development environment, TypeScript ensures our app is type-safe, and Tolgee simplifies internationalization (i18n), allowing our app to support multiple languages with ease.

In this tutorial, we'll walk through the process of building the weather app using:

  • Vite: For a fast, modern development experience.

  • TypeScript: For type safety.

  • Tolgee: To effortlessly manage translations.

  • OpenWeather API: To fetch real-time weather data.

Let's dive in!

Project Setup

Before we can start coding, let's get our project set up.

Install Vite:
Start by creating a new Vite project. Run the command below:

npm create vite@latest weather-app --template react-ts
cd weather-app
npm install

This sets up a Vite project using React and TypeScript.

Install TailwindCSS Next, let's install TailwindCSS, a utility-first CSS framework that provides us with a set of classes to style our app. Run:

npm install -D tailwindcss postcss autoprefixer

Initialize Tailwind CSS: After installation, initialize Tailwind CSS. This will create a tailwind.config.js file in your project:

npx tailwindcss init -p

Open the tailwind.config.js file add the following code:

module.exports = {
  content: ["./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

Add Tailwind’s directives to your CSS: Open the index.css file and add the following lines to include Tailwind’s base, components, and utilities:

   @tailwind base;
   @tailwind components;
   @tailwind utilities;

Install React Icons

To add icons to our app, we'll install React Icons. Run:

npm install react-icons

Install Tolgee:
Next, we'll install Tolgee. Tolgee makes it easy to implement in-context translation and edit translations directly in your app. Install it by running:

npm install @tolgee/react

Now visit the Tolgee website to create an account and get your API key.

Get your OpenWeather API key:

Sign up for an API key at OpenWeather.

Go to the root of the project and create a .env file to store your API keys gotten from the Tolgee and OpenWeather websites:

VITE_APP_TOLGEE_API_URL=https://app.tolgee.io
VITE_APP_TOLGEE_API_KEY=your-api-key
WEATHER_API_KEY=your-openweather-api-key

Quickly install type definitions for our app

npm i --save-dev @types/node

Open your vite.config.ts file and paste the code below

import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), "");
  return {
    define: {
      "process.env.VITE_APP_TOLGEE_API_URL": JSON.stringify(
        env.VITE_APP_TOLGEE_API_URL
      ),
      "process.env.VITE_APP_TOLGEE_API_KEY": JSON.stringify(
        env.VITE_APP_TOLGEE_API_KEY
      ),
      "process.env.WEATHER_API_KEY": JSON.stringify(env.WEATHER_API_KEY),
    },
    plugins: [react()],
  };
});

Building the App

With our project set up, it's time to dive into building the core functionality. Copy the following code into main.tsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Tolgee, DevTools, TolgeeProvider, FormatSimple } from "@tolgee/react";
import App from "./App.tsx";
import "./index.css";

const tolgee = Tolgee().use(DevTools()).use(FormatSimple()).init({
  language: "en",

  // for development
  apiUrl: process.env.VITE_APP_TOLGEE_API_URL,
  apiKey: process.env.VITE_APP_TOLGEE_API_KEY,
});

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <TolgeeProvider tolgee={tolgee} fallback="Loading...">
      <App />
    </TolgeeProvider>
  </StrictMode>
);

Now, in the root of your project, create a components folder and two files inside it - LanguageSelect.tsx and Loader.tsx

Inside the LanguageSelect.tsx file, add the following code:

import { T, useTolgee } from "@tolgee/react";

export default function LanguageSelect() {
  const tolgee = useTolgee(["language"]);
  return (
    <div className="flex items-center space-x-2">
      <label
        htmlFor="language-select"
        className="block text-sm font-medium text-gray-700 mb-1"
      >
        <T keyName="label">Change Language</T>
      </label>
      <select
        value={tolgee.getLanguage()}
        onChange={(e) => tolgee.changeLanguage(e.target.value)}
        className="border border-slate-500 z-50"
      >
        <option value="en">English</option>
        <option value="es">Español</option>
        <option value="ar">العربية</option>
        <option value="zh-Hans">中文</option>
      </select>
    </div>
  );
}

Inside the Loader.tsx file, add the following code:

// import React from 'react'

const RippleLoader = () => {
  return (
    <div className="lds-ripple relative inline-block w-20 h-20">
      <div className="absolute border-4 border-current rounded-full opacity-100 animate-ripple"></div>
      <div className="absolute border-4 border-current rounded-full opacity-100 animate-ripple delay-100"></div>

      <style>{`
        .lds-ripple div {
          animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
        }

        .lds-ripple div:nth-child(2) {
          animation-delay: -0.5s;
        }

        @keyframes lds-ripple {
          0% {
            top: 36px;
            left: 36px;
            width: 8px;
            height: 8px;
            opacity: 0;
          }
          4.9% {
            top: 36px;
            left: 36px;
            width: 8px;
            height: 8px;
            opacity: 0;
          }
          5% {
            top: 36px;
            left: 36px;
            width: 8px;
            height: 8px;
            opacity: 1;
          }
          100% {
            top: 0;
            left: 0;
            width: 80px;
            height: 80px;
            opacity: 0;
          }
        }
      `}</style>
    </div>
  );
};

export default RippleLoader;

Now open your App.tsx file and add the following code:

import { useState, useEffect } from "react";
import { T, useTranslate } from "@tolgee/react"; // Importing translation functions from Tolgee

import "./App.css";

import { FaMapMarkerAlt } from "react-icons/fa";
import { FaDroplet, FaWind } from "react-icons/fa6";
import { BiSearch } from "react-icons/bi";

import LanguageSelect from "./components/LanguageSelect";
import RippleLoader from "./components/Loader";

// Defining types for weather data
interface WeatherData {
  name: string;
  main: {
    temp: number;
    humidity: number;
  };
  weather: {
    description: string;
    icon: string;
  }[];
  wind: {
    speed: number;
  };
}

// Defining types for forecast data
interface ForecastData {
  list: {
    dt: number;
    main: {
      temp: number;
    };
    weather: {
      icon: string;
    }[];
  }[];
}

function App() {
  // State variables to store city name, weather data, forecast data, loading state, and errors
  const [city, setCity] = useState<string>(""); // City input value
  const [weatherData, setWeatherData] = useState<WeatherData | null>(null); // Current weather data
  const [forecastData, setForecastData] = useState<ForecastData | null>(null); // Forecast data
  const [loading, setLoading] = useState<boolean>(false); // Loading state
  const [error, setError] = useState<string | null>(null); // Error message

  const { t } = useTranslate(); // Tolgee translation hook

  // Function to fetch weather and forecast data from OpenWeather API
  const fetchWeatherData = async (cityName: string) => {
    const apiKey = process.env.WEATHER_API_KEY; // Fetching the API key from environment variables
    const currentWeatherUrl = `https://api.openweathermap.org/data/2.5/weather?q=${cityName}&appid=${apiKey}&units=metric`; // API URL for current weather
    const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?q=${cityName}&appid=${apiKey}&units=metric`; // API URL for 3-day forecast

    try {
      setLoading(true); // Setting loading state
      setError(null); // Resetting error state

      // Fetching current weather data
      const weatherResponse = await fetch(currentWeatherUrl);
      if (!weatherResponse.ok) {
        throw new Error("City not found! Try another one"); // Error if city not found
      }
      const weatherData: WeatherData = await weatherResponse.json(); // Parsing weather data
      setWeatherData(weatherData); // Setting the fetched weather data

      // Fetching forecast data
      const forecastResponse = await fetch(forecastUrl);
      if (!forecastResponse.ok) {
        throw new Error("Unable to fetch forecast data"); // Error if forecast data not available
      }
      const forecastData: ForecastData = await forecastResponse.json(); // Parsing forecast data
      setForecastData(forecastData); // Setting the fetched forecast data
    } catch (error) {
      // Handling any errors that occur during the fetch
      if (error instanceof Error) {
        setError(error.message); // Displaying error message
      }
      setWeatherData(null); // Resetting weather data
      setForecastData(null); // Resetting forecast data
    } finally {
      setLoading(false); // Resetting loading state
    }
  };

  // Handling form submission to fetch weather data based on city input
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    fetchWeatherData(city);
    setCity("");
  };

  // useEffect to fetch the user's location and display the weather for the current location
  useEffect(() => {
    const getUserLocation = async () => {
      if (navigator.geolocation) {
        // Checking if geolocation is available
        navigator.geolocation.getCurrentPosition(
          async (position) => {
            const { latitude, longitude } = position.coords; // Getting user's device location
            const apiKey = process.env.WEATHER_API_KEY;
            const reverseGeocodeUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=metric`; // API URL for reverse geocoding (get location by lat/lon

            try {
              setLoading(true);
              const response = await fetch(reverseGeocodeUrl); // Fetching weather data by geolocation
              if (!response.ok) {
                throw new Error("Unable to fetch location data"); // Error if location data fetch fails
              }
              const data: WeatherData = await response.json(); // Parsing weather data
              setWeatherData(data); // Setting the fetched weather data
              setCity(data.name); // Setting the city based on fetched location
              await fetchWeatherData(data.name); // Fetching weather data for the fetched city
            } catch (error) {
              // Handling any errors that occur during the fetch
              if (error instanceof Error) {
                setError(error.message); // Displaying error message
              }
              setWeatherData(null); // Resetting weather data
              setForecastData(null); // Resetting forecast data
            } finally {
              setLoading(false); // Resetting loading state
            }
          },
          () => {
            // Handling geolocation errors (e.g., if user denies location access)
            setError("Unable to retrieve your location");
            setWeatherData(null);
            setForecastData(null);
          }
        );
      } else {
        // Handling case where geolocation is not supported by the browser
        setError("Geolocation is not supported by this browser.");
        setWeatherData(null);
        setForecastData(null);
      }
    };

    getUserLocation(); // Invoking the function to get user location on page load
  }, []); // Empty dependency array to run effect once on component mount

  return (
    <>
      <section className="flex flex-col items-center justify-center min-h-screen px-3">
        <LanguageSelect />
        <div className="mt-3 bg-slate-400/50 rounded shadow-lg border border-white/30 p-5 w-full md:w-[350px]">
          <div>
            <form className="relative" onSubmit={handleSubmit}>
              <input
                type="text"
                name="search"
                placeholder={t("search_city", "Search City")} // Translation for placeholder text
                className="w-full px-4 py-2 border rounded-full text-white/70 bg-[#668ba0] focus:outline-none border-transparent focus:border-[#668ba0]"
                value={city}
                onChange={(e) => setCity(e.target.value)}
              />
              <button type="submit" className="absolute top-3 right-3">
                <BiSearch className="text-white/70" size={20} />
              </button>
            </form>
          </div>

          {loading && (
            <div className="flex justify-center items-center">
              <RippleLoader />
            </div>
          )}
          {error && (
            <p className="text-red-600 font-bold flex justify-center items-center">
              {error}
            </p>
          )}

          {!error && weatherData && (
            <div>
              {/* Displaying current weather data */}
              <div className="flex justify-between items-center text-white font-bold">
                <span className="flex items-center gap-x-2">
                  <FaMapMarkerAlt size={20} />
                  <p className="text-xl font-serif">{weatherData.name}</p>
                </span>

                <div className="flex flex-col items-center">
                  <img
                    src={`https://openweathermap.org/img/wn/${weatherData.weather[0].icon}@2x.png`}
                    alt="weather icon"
                  />{" "}
                  {/* Weather icon */}
                  <span>
                    <p className="text-6xl font-bold text-white">
                      {Math.round(weatherData.main.temp)} °C
                    </p>
                  </span>
                </div>
              </div>

              <div className="my-5 flex justify-between items-center">
                {/* Humidity */}
                <div className="flex items-center gap-x-3">
                  <FaDroplet size={30} className="text-white/90" />
                  <span>
                    <p className="text-lg font-serif text-white font-bold">
                      <T keyName="humidity">Humidity</T>
                    </p>
                    <p className="text-lg font-medium text-white/90">
                      {weatherData.main.humidity}%
                    </p>
                  </span>
                </div>

                {/* Wind Speed */}
                <div className="flex w-1/2 items-center gap-x-3">
                  <FaWind size={30} className="text-white/90" />
                  <span>
                    <p className="text-lg font-serif text-white font-bold">
                      <T keyName="wind_speed">Wind Speed</T>
                    </p>
                    <p className="text-lg font-medium text-white/90">
                      {weatherData.wind.speed} km/h
                    </p>
                  </span>
                </div>
              </div>
            </div>
          )}
        </div>
      </section>
    </>
  );
}

export default App;

In our code above, we wrapped some of the tags with the T tag from Tolgee to be able to provide multilingual support. We also used the useTolgee hook to get the current language and change the language when the user selects a different option.

Finally, open your App.css file and add the following styles:

body {
    background-color: #98c7da; /* Replace with your desired background color */
    background-image: url("./assets/mountain-view.jpg");  /* Replace with the file path to your background image */
    background-repeat: no-repeat;
    background-position: center;
    background-size: cover;
    position: relative;
    overflow: hidden;
}

body::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-image: inherit;
    background-repeat: no-repeat;
    background-position: inherit;
    background-size: inherit;
    filter: blur(5px);
    z-index: -1;
}

Now open the app in your browser http://localhost:5173/, you should see something like this:

App screenshot

Add translation support to our app

On your browser, hold down your ALT key and hover over the "Forecast for the next 3 days" text or any other text we wrapped with the T tag and tap on it. A popup should appear like so:

In context app translation

Write down the text you want to be translated in the English text box and scroll down and click the create button

Go to your Tolgee app dashboard, click on the translations tab and locate the new key you just created

Tolgee App Dashboard

On the right as in the picture above, select the language and choose the translation and then hit save. Now do the same for the other text wrapped with the T tag and there we've successfully added translation support to our app with Tolgee.

Conclusion

We've successfully built a weather app using Vite, TypeScript and TailwindCSS and integrated with the OpenWeather API to provide real-time weather data. With Tolgee, we added multilingual support, making the app accessible to users in different regions. This project demonstrates how modern tools can help us quickly build and scale applications while ensuring internationalization.

Got any questions? Ask in the comments

Happy Hacking!

0
Subscribe to my newsletter

Read articles from Anietie Brownson directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anietie Brownson
Anietie Brownson