aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/lib/i18n
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-11-17 00:33:28 +0000
committerGitHub <noreply@github.com>2024-11-17 00:33:28 +0000
commit4354ee7ba1c6ac9a9567944ae6169b1664e0ea8a (patch)
treee27c9070930514d77582bae00b3350274116179c /apps/web/lib/i18n
parent9f2c7be23769bb0f4102736a683710b1a1939661 (diff)
downloadkarakeep-4354ee7ba1c6ac9a9567944ae6169b1664e0ea8a.tar.zst
feature: Add i18n support. Fixes #57 (#635)
* feature(web): Add basic scaffolding for i18n * refactor: Switch most of the app's strings to use i18n strings * fix: Remove unused i18next-resources-for-ts command * Add user setting * More translations * Drop the german translation for now
Diffstat (limited to 'apps/web/lib/i18n')
-rw-r--r--apps/web/lib/i18n/client.ts33
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json206
-rw-r--r--apps/web/lib/i18n/provider.tsx18
-rw-r--r--apps/web/lib/i18n/server.ts36
-rw-r--r--apps/web/lib/i18n/settings.ts17
5 files changed, 310 insertions, 0 deletions
diff --git a/apps/web/lib/i18n/client.ts b/apps/web/lib/i18n/client.ts
new file mode 100644
index 00000000..1c56a88a
--- /dev/null
+++ b/apps/web/lib/i18n/client.ts
@@ -0,0 +1,33 @@
+"use client";
+
+import i18next from "i18next";
+import resourcesToBackend from "i18next-resources-to-backend";
+import {
+ initReactI18next,
+ useTranslation as useTranslationOrg,
+} from "react-i18next";
+
+import { getOptions, languages } from "./settings";
+
+const runsOnServerSide = typeof window === "undefined";
+
+i18next
+ .use(initReactI18next)
+ .use(
+ resourcesToBackend(
+ (language: string, namespace: string) =>
+ import(`./locales/${language}/${namespace}.json`),
+ ),
+ )
+ .init({
+ ...getOptions(),
+ lng: undefined, // let detect the language on client side
+ debug: false,
+ interpolation: {
+ escapeValue: false, // not needed for react as it escapes by default
+ },
+ preload: runsOnServerSide ? languages : [],
+ });
+
+export const useTranslation = useTranslationOrg;
+export const i18n = i18next;
diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json
new file mode 100644
index 00000000..530d489a
--- /dev/null
+++ b/apps/web/lib/i18n/locales/en/translation.json
@@ -0,0 +1,206 @@
+{
+ "common": {
+ "url": "URL",
+ "name": "Name",
+ "email": "Email",
+ "password": "Password",
+ "action": "Action",
+ "actions": "Actions",
+ "created_at": "Created At",
+ "key": "Key",
+ "role": "Role",
+ "roles": {
+ "user": "User",
+ "admin": "Admin"
+ },
+ "something_went_wrong": "Something went wrong",
+ "experimental": "Experimental",
+ "search": "Search",
+ "tags": "Tags",
+ "note": "Note",
+ "attachments": "Attachments",
+ "screenshot": "Screenshot",
+ "video": "Video",
+ "archive": "Archive",
+ "home": "Home"
+ },
+ "layouts": {
+ "masonry": "Masonry",
+ "grid": "Grid",
+ "list": "List",
+ "compact": "Compact"
+ },
+ "actions": {
+ "change_layout": "Change Layout",
+ "archive": "Archive",
+ "unarchive": "Un-archive",
+ "favorite": "Favorite",
+ "unfavorite": "Unfavorite",
+ "delete": "Delete",
+ "refresh": "Refresh",
+ "download_full_page_archive": "Download Full Page Archive",
+ "edit_tags": "Edit Tags",
+ "add_to_list": "Add to List",
+ "select_all": "Select All",
+ "unselect_all": "Unselect All",
+ "copy_link": "Copy Link",
+ "close_bulk_edit": "Close Bulk Edit",
+ "bulk_edit": "Bulk Edit",
+ "manage_lists": "Manage Lists",
+ "remove_from_list": "Remove from List",
+ "save": "Save",
+ "add": "Add",
+ "edit": "Edit",
+ "create": "Create",
+ "fetch_now": "Fetch Now",
+ "summarize_with_ai": "Summarize with AI",
+ "edit_title": "Edit Title",
+ "sign_out": "Sign Out",
+ "close": "Close",
+ "merge": "Merge",
+ "cancel": "Cancel",
+ "apply_all": "Apply All",
+ "ignore": "Ignore"
+ },
+ "settings": {
+ "back_to_app": "Back To App",
+ "user_settings": "User Settings",
+ "info": {
+ "user_info": "User Info",
+ "basic_details": "Basic Details",
+ "change_password": "Change Password",
+ "current_password": "Current Password",
+ "new_password": "New Password",
+ "confirm_new_password": "Confirm New Password",
+ "options": "Options",
+ "interface_lang": "Interface Language"
+ },
+ "ai": {
+ "ai_settings": "AI Settings",
+ "tagging_rules": "Tagging Rules",
+ "tagging_rule_description": "Prompts that you add here will be included as rules to the model during tag generation. You can view the final prompts in the prompt preview section.",
+ "prompt_preview": "Prompt Preview",
+ "text_prompt": "Text Prompt",
+ "images_prompt": "Image Prompt"
+ },
+ "feeds": {
+ "rss_subscriptions": "RSS Subscriptions",
+ "add_a_subscription": "Add a Subscription"
+ },
+ "import": {
+ "import_export": "Import / Export",
+ "import_export_bookmarks": "Import / Export Bookmarks",
+ "import_bookmarks_from_html_file": "Import Bookmarks from HTML file",
+ "import_bookmarks_from_pocket_export": "Import Bookmarks from Pocket export",
+ "import_bookmarks_from_omnivore_export": "Import Bookmarks from Omnivore export",
+ "import_bookmarks_from_hoarder_export": "Import Bookmarks from Hoarder export",
+ "export_links_and_notes": "Export Links and Notes",
+ "imported_bookmarks": "Imported Bookmarks"
+ },
+ "api_keys": {
+ "api_keys": "API Keys",
+ "new_api_key": "New API Key",
+ "new_api_key_desc": "Give your API key a unique name",
+ "key_success": "Key was successfully created",
+ "key_success_please_copy": "Please copy the key and store it somewhere safe. Once you close the dialog, you won't be able to access it again."
+ }
+ },
+ "admin": {
+ "admin_settings": "Admin Settings",
+ "server_stats": {
+ "server_stats": "Server Stats",
+ "total_users": "Total Users",
+ "total_bookmarks": "Total Bookmarks",
+ "server_version": "Server Version"
+ },
+ "background_jobs": {
+ "background_jobs": "Background Jobs",
+ "crawler_jobs": "Crawler Jobs",
+ "indexing_jobs": "Indexing Jobs",
+ "inference_jobs": "Inference Jobs",
+ "tidy_assets_jobs": "Tidy Assets Jobs",
+ "job": "Job",
+ "queued": "Queued",
+ "pending": "Pending",
+ "failed": "Failed"
+ },
+ "actions": {
+ "recrawl_failed_links_only": "Recrawl Failed Links Only",
+ "recrawl_all_links": "Recrawl All Links",
+ "without_inference": "Without Inference",
+ "regenerate_ai_tags_for_failed_bookmarks_only": "Regenerate AI Tags for Failed Bookmarks Only",
+ "regenerate_ai_tags_for_all_bookmarks": "Regenerate AI Tags for All Bookmarks",
+ "reindex_all_bookmarks": "Reindex All Bookmarks",
+ "compact_assets": "Compact Assets"
+ },
+ "users_list": {
+ "users_list": "Users List",
+ "create_user": "Create User",
+ "change_role": "Change Role",
+ "reset_password": "Reset Password",
+ "delete_user": "Delete User",
+ "num_bookmarks": "Num Bookmarks",
+ "asset_sizes": "Asset Sizes",
+ "local_user": "Local User",
+ "confirm_password": "Confirm Password"
+ }
+ },
+ "options": {
+ "dark_mode": "Dark Mode",
+ "light_mode": "Light Mode"
+ },
+ "lists": {
+ "all_lists": "All Lists",
+ "favourites": "Favourites",
+ "new_list": "New List",
+ "new_nested_list": "New Nested List"
+ },
+ "tags": {
+ "all_tags": "All Tags",
+ "your_tags": "Your Tags",
+ "your_tags_info": "Tags that were attached at least once by you",
+ "ai_tags": "AI Tags",
+ "ai_tags_info": "Tags that were only attached automatically (by AI)",
+ "unused_tags": "Unused Tags",
+ "unused_tags_info": "Tags that are not attached to any bookmarks",
+ "delete_all_unused_tags": "Delete All Unused Tags",
+ "drag_and_drop_merging": "Drag & Drop Merging",
+ "drag_and_drop_merging_info": "Drag and drop tags on each other to merge them",
+ "sort_by_name": "Sort by Name"
+ },
+ "preview": {
+ "view_original": "View Original",
+ "cached_content": "Cached Content"
+ },
+ "editor": {
+ "quickly_focus": "You can quickly focus on this field by pressing ⌘ + E",
+ "multiple_urls_dialog_title": "Importing URLs as separate Bookmarks?",
+ "multiple_urls_dialog_desc": "The input contains multiple URLs on separate lines. Do you want to import them as separate bookmarks?",
+ "import_as_text": "Import as Text Bookmark",
+ "import_as_separate_bookmarks": "Import as separate Bookmarks",
+ "placeholder": "Paste a link or an image, write a note or drag and drop an image in here ...",
+ "new_item": "NEW ITEM",
+ "disabled_submissions": "Submissions are disabled"
+ },
+ "toasts": {
+ "bookmarks": {
+ "updated": "The bookmark has been updated!",
+ "deleted": "The bookmark has been deleted!",
+ "refetch": "Re-fetch has been enqueued!",
+ "full_page_archive": "Full Page Archive creation has been triggered",
+ "delete_from_list": "The bookmark has been deleted from the list",
+ "clipboard_copied": "Link has been added to your clipboard!"
+ },
+ "lists": {
+ "created": "List has been created!",
+ "updated": "List has been updated!"
+ }
+ },
+ "cleanups": {
+ "cleanups": "Cleanups",
+ "duplicate_tags": {
+ "title": "Duplicate Tags",
+ "merge_all_suggestions": "Merge all suggestions?"
+ }
+ }
+}
diff --git a/apps/web/lib/i18n/provider.tsx b/apps/web/lib/i18n/provider.tsx
new file mode 100644
index 00000000..303e24bf
--- /dev/null
+++ b/apps/web/lib/i18n/provider.tsx
@@ -0,0 +1,18 @@
+import { i18n } from "@/lib/i18n/client";
+import { I18nextProvider } from "react-i18next";
+
+const CustomI18nextProvider = ({
+ lang,
+ children,
+}: {
+ lang: string;
+ children: React.ReactNode;
+}) => {
+ if (i18n.language !== lang) {
+ i18n.changeLanguage(lang);
+ }
+
+ return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
+};
+
+export default CustomI18nextProvider;
diff --git a/apps/web/lib/i18n/server.ts b/apps/web/lib/i18n/server.ts
new file mode 100644
index 00000000..0473fd77
--- /dev/null
+++ b/apps/web/lib/i18n/server.ts
@@ -0,0 +1,36 @@
+import { getUserLocalSettings } from "@/lib/userLocalSettings/userLocalSettings";
+import { createInstance, FlatNamespace, KeyPrefix } from "i18next";
+import resourcesToBackend from "i18next-resources-to-backend";
+import { FallbackNs } from "react-i18next";
+import { initReactI18next } from "react-i18next/initReactI18next";
+
+import { getOptions } from "./settings";
+
+const initI18next = async (lng: string, ns: string | string[]) => {
+ const i18nInstance = createInstance();
+ await i18nInstance
+ .use(initReactI18next)
+ .use(
+ resourcesToBackend(
+ (language: string, namespace: string) =>
+ import(`./locales/${language}/${namespace}.json`),
+ ),
+ )
+ .init(getOptions(lng, ns?.toString()));
+ return i18nInstance;
+};
+
+export async function useTranslation<
+ Ns extends FlatNamespace,
+ KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,
+>(ns?: Ns, options: { keyPrefix?: KPrefix } = {}) {
+ const lng = (await getUserLocalSettings()).lang;
+ const i18nextInstance = await initI18next(
+ lng,
+ Array.isArray(ns) ? (ns as string[]) : (ns as string),
+ );
+ return {
+ t: i18nextInstance.getFixedT(lng, ns as FlatNamespace, options.keyPrefix),
+ i18n: i18nextInstance,
+ };
+}
diff --git a/apps/web/lib/i18n/settings.ts b/apps/web/lib/i18n/settings.ts
new file mode 100644
index 00000000..5787a55e
--- /dev/null
+++ b/apps/web/lib/i18n/settings.ts
@@ -0,0 +1,17 @@
+import { supportedLangs } from "@hoarder/shared/langs";
+
+export const fallbackLng = "en";
+export const languages = supportedLangs;
+export const defaultNS = "translation";
+export const cookieName = "i18next";
+
+export function getOptions(lng: string = fallbackLng, ns: string = defaultNS) {
+ return {
+ supportedLngs: languages,
+ fallbackLng,
+ lng,
+ fallbackNS: defaultNS,
+ defaultNS,
+ ns,
+ };
+}