From a50e003719774b4bec1d179867d27df2333a231e Mon Sep 17 00:00:00 2001 From: Ailton Heusser Junior Date: Sat, 12 Sep 2026 07:30:13 -0300 Subject: [PATCH] feat: add Brazilian Portuguese localization foundation Signed-off-by: Ailton Heusser Junior --- package.json | 2 + pnpm-lock.yaml | 50 +++++++ src/app/LanguageSettings.tsx | 49 +++++++ src/app/Settings.tsx | 21 ++- src/app/navigation.ts | 2 +- src/app/shell/AppShell.tsx | 11 +- src/app/shell/Home.tsx | 17 ++- src/app/shell/PageSearch.tsx | 26 ++-- src/app/shell/ProfileButton.tsx | 12 +- src/features/localization/service.test.ts | 35 +++++ src/features/localization/service.ts | 153 ++++++++++++++++++++++ tests/browser/settings.spec.mjs | 34 +++++ 12 files changed, 381 insertions(+), 31 deletions(-) create mode 100644 src/app/LanguageSettings.tsx create mode 100644 src/features/localization/service.test.ts create mode 100644 src/features/localization/service.ts diff --git a/package.json b/package.json index ec19fb25..d101aa77 100644 --- a/package.json +++ b/package.json @@ -48,11 +48,13 @@ "dockview-react": "8.2.0", "emoji-mart": "5.6.0", "flexlayout-react": "0.10.8", + "i18next": "^26.4.2", "lucide-react": "1.34.0", "mdast-util-from-markdown": "2.0.3", "nostr-tools": "2.25.0", "react": "19.2.8", "react-dom": "19.2.8", + "react-i18next": "^17.0.13", "react-markdown": "10.1.0", "remark-breaks": "4.0.0", "remark-gfm": "4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bad39e68..9435fe4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: flexlayout-react: specifier: 0.10.8 version: 0.10.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + i18next: + specifier: ^26.4.2 + version: 26.4.2(typescript@7.0.2) lucide-react: specifier: 1.34.0 version: 1.34.0(react@19.2.8) @@ -62,6 +65,9 @@ importers: react-dom: specifier: 19.2.8 version: 19.2.8(react@19.2.8) + react-i18next: + specifier: ^17.0.13 + version: 17.0.13(i18next@26.4.2(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.2.18)(react@19.2.8) @@ -950,9 +956,20 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + html-parse-stringify@4.0.1: + resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + i18next@26.4.2: + resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==} + peerDependencies: + typescript: ^5 || ^6 || ^7 + peerDependenciesMeta: + typescript: + optional: true + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -1332,6 +1349,22 @@ packages: peerDependencies: react: ^19.2.8 + react-i18next@17.0.13: + resolution: {integrity: sha512-Cc1PscmblIHA1kljTqDwrcVMI21ydgmUzw0UAeQBe7pAOgfuRLfzXze4EUBQoeDiICzFIXXhHFoZxuetNg5D0Q==} + peerDependencies: + i18next: '>= 26.2.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 || ^6 || ^7 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + react-markdown@10.1.0: resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} peerDependencies: @@ -2202,8 +2235,14 @@ snapshots: dependencies: '@types/hast': 3.0.5 + html-parse-stringify@4.0.1: {} + html-url-attributes@3.0.1: {} + i18next@26.4.2(typescript@7.0.2): + optionalDependencies: + typescript: 7.0.2 + inline-style-parser@0.2.7: {} is-alphabetical@2.0.1: {} @@ -2739,6 +2778,17 @@ snapshots: react: 19.2.8 scheduler: 0.27.0 + react-i18next@17.0.13(i18next@26.4.2(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2): + dependencies: + '@babel/runtime': 7.29.7 + html-parse-stringify: 4.0.1 + i18next: 26.4.2(typescript@7.0.2) + react: 19.2.8 + use-sync-external-store: 1.6.0(react@19.2.8) + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + typescript: 7.0.2 + react-markdown@10.1.0(@types/react@19.2.18)(react@19.2.8): dependencies: '@types/hast': 3.0.5 diff --git a/src/app/LanguageSettings.tsx b/src/app/LanguageSettings.tsx new file mode 100644 index 00000000..f20743bd --- /dev/null +++ b/src/app/LanguageSettings.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from "react-i18next"; +import { + appLocales, + currentLocale, + setAppLocale, + type AppLocale, +} from "../features/localization/service"; + +export function LanguageSettings() { + const { t } = useTranslation(); + return ( +
+

+ {t("language.title")} +

+
+ +

+ {t("language.description")} +

+ +
+
+ ); +} diff --git a/src/app/Settings.tsx b/src/app/Settings.tsx index 5fdba59f..5ec386be 100644 --- a/src/app/Settings.tsx +++ b/src/app/Settings.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useSyncExternalStore } from "react"; import { RecoveryScreen } from "./RecoveryScreen"; -import { Blocks, Settings2, UserRound, Palette } from "lucide-react"; +import { Blocks, Settings2, UserRound, Palette, Languages } from "lucide-react"; import type { PluginManager } from "../plugins/manager"; import type { Communities } from "../features/communities/service"; import { PluginImport } from "./PluginImport"; @@ -8,11 +8,14 @@ import { ProfileSettings } from "./ProfileSettings"; import type { Appearance } from "../shared/theme/service"; import { AppearanceSettings } from "./AppearanceSettings"; +import { LanguageSettings } from "./LanguageSettings"; +import { useTranslation } from "react-i18next"; const sections = [ - { id: "profile", label: "Profile", icon: UserRound }, - { id: "plugins", label: "Plugins", icon: Blocks }, - { id: "appearance", label: "Appearance", icon: Palette }, + { id: "profile", label: "settings.profile", icon: UserRound }, + { id: "plugins", label: "settings.plugins", icon: Blocks }, + { id: "appearance", label: "settings.appearance", icon: Palette }, + { id: "language", label: "settings.language", icon: Languages }, ] as const; export function Settings({ @@ -30,6 +33,7 @@ export function Settings({ | undefined; onSection?: (section: string) => void; }) { + const { t } = useTranslation(); const [selected, setSelected] = useState<(typeof sections)[number]["id"]>("profile"); const requestedSection = @@ -63,13 +67,13 @@ export function Settings({ id="settings-title" className="m-0 text-3xl font-medium tracking-tight" > - Settings + {t("settings.title")}
@@ -96,6 +100,9 @@ export function Settings({ + -