Add i18n support #5
@ -1 +1,2 @@
|
||||
src/lib/api.d.ts
|
||||
src/i18n/locales/*/*.ts
|
||||
15
frontend/.swcrc
Normal file
15
frontend/.swcrc
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://swc.rs/schema.json",
|
||||
"jsc": {
|
||||
"experimental": {
|
||||
"plugins": [
|
||||
[
|
||||
"@lingui/swc-plugin",
|
||||
{
|
||||
// Additional Configuration
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,63 @@
|
||||
|
||||
## Developing
|
||||
|
||||
## i18n
|
||||
|
||||
This project uses [Lingui](https://lingui.js.org/) for internalization. The translations are stored in the `src/i18n/locales` folder.
|
||||
|
||||
**Translating in JSX: `<Trans>` Component**
|
||||
|
||||
Use the `<Trans>` component when translating strings directly in JSX markup:
|
||||
|
||||
```jsx
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
|
||||
function Greeting() {
|
||||
return (
|
||||
<h1>
|
||||
<Trans>Hello, world!</Trans>
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Translating in JavaScript: `i18n._(t\`...`)`**
|
||||
|
||||
For translations used **outside of JSX**, such as in variables, logs, or logic, use `i18n._(t\`...`)`:
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { i18n } from "@lingui/core";
|
||||
|
||||
console.log(i18n._(t`Operation completed successfully.`));
|
||||
```
|
||||
|
||||
### Extracting & Compiling Translations
|
||||
|
||||
#### Extract Strings
|
||||
|
||||
To extract translatable strings into `.po` files:
|
||||
|
||||
```bash
|
||||
# To extract all translations
|
||||
npm run extract
|
||||
|
||||
# To extract translations and clean up unused ones
|
||||
npm run extract-clean
|
||||
```
|
||||
|
||||
This updates the `messages.po` files inside `src/i18n/locales/{locale}/`.
|
||||
|
||||
#### Compile Translations
|
||||
|
||||
After updating the `.po` files with your translations, compile them into `.ts` files:
|
||||
|
||||
```bash
|
||||
npm run compile
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node v20
|
||||
|
||||
@ -6,7 +6,7 @@ import tseslint from "typescript-eslint";
|
||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{ ignores: ["dist", "src/i18n/locales/**/*.ts"] },
|
||||
|
ansv7779 marked this conversation as resolved
|
||||
{
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
|
||||
12
frontend/lingui.config.js
Normal file
12
frontend/lingui.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "@lingui/cli";
|
||||
|
||||
export default defineConfig({
|
||||
sourceLocale: "en",
|
||||
locales: ["sv", "en"],
|
||||
catalogs: [
|
||||
{
|
||||
path: "<rootDir>/src/i18n/locales/{locale}/messages",
|
||||
include: ["src"],
|
||||
},
|
||||
],
|
||||
});
|
||||
3626
frontend/package-lock.json
generated
3626
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,13 +5,17 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"build": "lingui compile --typescript && tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier . --write",
|
||||
"update-api": "openapi-typescript http://localhost:8080/v3/api-docs --output src/lib/api.d.ts",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"extract": "lingui extract",
|
||||
"extract-clean": "lingui extract --clean",
|
||||
"compile": "lingui compile --typescript"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/react": "^5.3.0",
|
||||
"openapi-fetch": "^0.13.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@ -19,6 +23,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@lingui/cli": "^5.3.0",
|
||||
"@lingui/swc-plugin": "^5.5.1",
|
||||
"@lingui/vite-plugin": "^5.3.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||
|
||||
@ -3,16 +3,23 @@ import suLogoLandscape from "./assets/SU_logo_optimized.svg";
|
||||
import { ProfileContext } from "./hooks/profile.ts";
|
||||
import Studentportalen from "./Studentportalen.tsx";
|
||||
import { useViewTransitioningFetch } from "./hooks/fetch";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useEffect } from "react";
|
||||
import { dynamicActivate } from "./i18n/dynamicActivate.ts";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { i18n } from "@lingui/core";
|
||||
|
||||
function App() {
|
||||
const { data: profile, error } = useViewTransitioningFetch("/profile");
|
||||
|
||||
if (!profile) {
|
||||
return splashScreen("Loading...");
|
||||
}
|
||||
if (error) {
|
||||
return splashScreen("Application failed to start");
|
||||
useEffect(() => {
|
||||
if (profile?.language) {
|
||||
dynamicActivate(profile.language);
|
||||
}
|
||||
}, [profile?.language]);
|
||||
|
||||
if (!profile) return splashScreen(i18n._(t`Loading...`));
|
||||
if (error) return splashScreen(i18n._(t`Application failed to start`));
|
||||
|
||||
return (
|
||||
<ProfileContext value={profile}>
|
||||
@ -23,12 +30,16 @@ function App() {
|
||||
|
||||
function splashScreen(extraContent: string) {
|
||||
return (
|
||||
<div id={"app"}>
|
||||
<img src={suLogoLandscape} alt={"Stockholm University"} />
|
||||
<div id="app">
|
||||
<img src={suLogoLandscape} alt={i18n._(t`Stockholm University`)} />
|
||||
<div>
|
||||
<h1>Student portal</h1>
|
||||
<h2>Department of Computer and Systems Sciences</h2>
|
||||
{extraContent}
|
||||
<h1>
|
||||
<Trans>Student portal</Trans>
|
||||
</h1>
|
||||
<h2>
|
||||
<Trans>Department of Computer and Systems Sciences</Trans>
|
||||
</h2>
|
||||
<p>{extraContent}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
12
frontend/src/i18n/dynamicActivate.ts
Normal file
12
frontend/src/i18n/dynamicActivate.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { i18n } from "@lingui/core";
|
||||
|
||||
/**
|
||||
* We do a dynamic import of just the catalog that we need.
|
||||
* Source: https://lingui.dev/guides/dynamic-loading-catalogs
|
||||
* @param locale any locale string
|
||||
*/
|
||||
export async function dynamicActivate(locale: string) {
|
||||
const { messages } = await import(`./locales/${locale}/messages.po`);
|
||||
i18n.load(locale, messages);
|
||||
i18n.activate(locale);
|
||||
}
|
||||
15
frontend/src/i18n/getDefaultLocale.ts
Normal file
15
frontend/src/i18n/getDefaultLocale.ts
Normal file
@ -0,0 +1,15 @@
|
||||
const supportedLocales = ["sv", "en"];
|
||||
|
||||
export function getDefaultLocale(): string {
|
||||
const userPreferredLangs = navigator.languages.map(
|
||||
(lang) => lang.split("-")[0],
|
||||
);
|
||||
|
||||
for (const lang of userPreferredLangs) {
|
||||
if (supportedLocales.includes(lang)) {
|
||||
return lang;
|
||||
}
|
||||
}
|
||||
|
||||
return "en"; // fallback to English
|
||||
}
|
||||
55
frontend/src/i18n/locales/en/messages.po
Normal file
55
frontend/src/i18n/locales/en/messages.po
Normal file
@ -0,0 +1,55 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2025-04-07 19:53+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: en\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/App.tsx:22
|
||||
msgid "Application failed to start"
|
||||
msgstr "Application failed to start"
|
||||
|
||||
#: src/App.tsx:40
|
||||
msgid "Department of Computer and Systems Sciences"
|
||||
msgstr "Department of Computer and Systems Sciences"
|
||||
|
||||
#: src/studentportalen/Home.tsx:10
|
||||
msgid "Here you can see the latest and greatest"
|
||||
msgstr "Here you can see the latest and greatest"
|
||||
|
||||
#: src/studentportalen/Menu.tsx:12
|
||||
msgid "Home"
|
||||
msgstr "Home"
|
||||
|
||||
#: src/studentportalen/Home.tsx:7
|
||||
msgid "Home screen"
|
||||
msgstr "Home screen"
|
||||
|
||||
#: src/App.tsx:21
|
||||
msgid "Loading..."
|
||||
msgstr "Loading..."
|
||||
|
||||
#: src/studentportalen/Menu.tsx:24
|
||||
msgid "Log out"
|
||||
msgstr "Log out"
|
||||
|
||||
#: src/studentportalen/Menu.tsx:21
|
||||
msgid "Profile"
|
||||
msgstr "Profile"
|
||||
|
||||
#: src/App.tsx:34
|
||||
#: src/studentportalen/Header.tsx:12
|
||||
msgid "Stockholm University"
|
||||
msgstr "Stockholm University"
|
||||
|
||||
#: src/App.tsx:37
|
||||
msgid "Student portal"
|
||||
msgstr "Student portal"
|
||||
1
frontend/src/i18n/locales/en/messages.ts
Normal file
1
frontend/src/i18n/locales/en/messages.ts
Normal file
@ -0,0 +1 @@
|
||||
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"3C3CY2\":[\"Application failed to start\"],\"xYQvrP\":[\"Department of Computer and Systems Sciences\"],\"z13C6e\":[\"Here you can see the latest and greatest\"],\"i0qMbr\":[\"Home\"],\"p7H6+B\":[\"Home screen\"],\"Z3FXyt\":[\"Loading...\"],\"FgAxTj\":[\"Log out\"],\"vERlcd\":[\"Profile\"],\"he8WSY\":[\"Stockholm University\"],\"07DTL2\":[\"Student portal\"]}")as Messages;
|
||||
55
frontend/src/i18n/locales/sv/messages.po
Normal file
55
frontend/src/i18n/locales/sv/messages.po
Normal file
@ -0,0 +1,55 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2025-04-07 19:53+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: sv\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/App.tsx:22
|
||||
msgid "Application failed to start"
|
||||
msgstr "Applikationen startade inte"
|
||||
|
||||
#: src/App.tsx:40
|
||||
msgid "Department of Computer and Systems Sciences"
|
||||
msgstr "Institutionen för data- och systemvetenskap"
|
||||
|
||||
#: src/studentportalen/Home.tsx:10
|
||||
msgid "Here you can see the latest and greatest"
|
||||
msgstr "Här kan du hitta det senaste och bästa"
|
||||
|
||||
#: src/studentportalen/Menu.tsx:12
|
||||
msgid "Home"
|
||||
msgstr "Hem"
|
||||
|
||||
#: src/studentportalen/Home.tsx:7
|
||||
msgid "Home screen"
|
||||
msgstr "Hemskärm"
|
||||
|
||||
#: src/App.tsx:21
|
||||
msgid "Loading..."
|
||||
msgstr "Laddar..."
|
||||
|
||||
#: src/studentportalen/Menu.tsx:24
|
||||
msgid "Log out"
|
||||
msgstr "Logga ut"
|
||||
|
||||
#: src/studentportalen/Menu.tsx:21
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/App.tsx:34
|
||||
#: src/studentportalen/Header.tsx:12
|
||||
msgid "Stockholm University"
|
||||
msgstr "Stockholms universitet"
|
||||
|
||||
#: src/App.tsx:37
|
||||
msgid "Student portal"
|
||||
msgstr "Studentportal"
|
||||
1
frontend/src/i18n/locales/sv/messages.ts
Normal file
1
frontend/src/i18n/locales/sv/messages.ts
Normal file
@ -0,0 +1 @@
|
||||
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"3C3CY2\":[\"Applikationen startade inte\"],\"xYQvrP\":[\"Institutionen för data- och systemvetenskap\"],\"z13C6e\":[\"Här kan du hitta det senaste och bästa\"],\"i0qMbr\":[\"Hem\"],\"p7H6+B\":[\"Hemskärm\"],\"Z3FXyt\":[\"Laddar...\"],\"FgAxTj\":[\"Logga ut\"],\"vERlcd\":[\"Profil\"],\"he8WSY\":[\"Stockholms universitet\"],\"07DTL2\":[\"Studentportal\"]}")as Messages;
|
||||
@ -2,9 +2,19 @@ import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { dynamicActivate } from "./i18n/dynamicActivate.ts";
|
||||
import { getDefaultLocale } from "./i18n/getDefaultLocale.ts";
|
||||
|
||||
// Activate the default locale from browser settings before a profile is loaded
|
||||
const locale = getDefaultLocale();
|
||||
await dynamicActivate(locale);
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<App />
|
||||
</I18nProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import "./header.css";
|
||||
import suLogo from "../assets/SU_logo_optimized.svg";
|
||||
import { useProfile } from "../hooks/profile.ts";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { i18n } from "@lingui/core";
|
||||
|
||||
export default function Header() {
|
||||
const profile = useProfile();
|
||||
|
||||
return (
|
||||
<header>
|
||||
<img src={suLogo} alt="Stockholm University" />
|
||||
<img src={suLogo} alt={i18n._(t`Stockholm University`)} />
|
||||
|
||||
<span>{profile.name}</span>
|
||||
</header>
|
||||
);
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<h1>Home screen</h1>
|
||||
<p>Here you can see the latest and greatest</p>
|
||||
<h1>
|
||||
<Trans>Home screen</Trans>
|
||||
</h1>
|
||||
<p>
|
||||
<Trans>Here you can see the latest and greatest</Trans>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,21 +1,28 @@
|
||||
import { NavLink } from "react-router";
|
||||
import "./menu.css";
|
||||
import { useState } from "react";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
|
||||
export default function Menu() {
|
||||
const [subMenuVisible, setSubMenuVisible] = useState(false);
|
||||
return (
|
||||
<menu className={"main"}>
|
||||
<li>
|
||||
<NavLink to={"/"}>Home</NavLink>
|
||||
<NavLink to={"/"}>
|
||||
<Trans>Home</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => setSubMenuVisible((current) => !current)}>
|
||||
<span>| | |</span>
|
||||
</button>
|
||||
<menu className={"sub " + (subMenuVisible ? "visible" : "")}>
|
||||
<li>Profile</li>
|
||||
<li>Log out</li>
|
||||
<li>
|
||||
<Trans>Profile</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Log out</Trans>
|
||||
</li>
|
||||
</menu>
|
||||
</li>
|
||||
</menu>
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { lingui } from "@lingui/vite-plugin";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
target: "esnext",
|
||||
},
|
||||
plugins: [
|
||||
react({
|
||||
plugins: [["@lingui/swc-plugin", {}]],
|
||||
}),
|
||||
lingui(),
|
||||
],
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user
Do we need this since the generated
.tsfiles have/*eslint-disable*/at the start?Otherwise you get the warning
Unused eslint-disable directive (no problems were reported)when runnningnpm run lint