Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-2870]feat: language support #6215

Open
wants to merge 20 commits into
base: preview
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
607ca5a
fix: adding language support package
sriramveeraghanta Dec 15, 2024
254db2d
Merge branch 'preview' of github.com:makeplane/plane into feat-langua…
sriramveeraghanta Dec 16, 2024
2dab6c0
fix: language support implementation using mobx
sriramveeraghanta Dec 16, 2024
f311264
fix: adding more languages for support
sriramveeraghanta Dec 16, 2024
194003d
fix: profile settings translations
sriramveeraghanta Dec 16, 2024
07c5147
feat: added language support for sidebar and user settings
mathalav55 Dec 17, 2024
e1fede3
feat: added language support for deactivation modal
mathalav55 Dec 17, 2024
28c5e3f
fix: added project sync after transfer issues (#6200)
mathalav55 Dec 16, 2024
8800b2f
code refactor and improvement (#6203)
anmolsinghbhatia Dec 16, 2024
23dc6f5
refactor: enhance workspace and project wrapper modularity (#6207)
prateekshourya29 Dec 16, 2024
4d13b0b
[WEB-2678]feat: added functionality to add labels directly from dropd…
mathalav55 Dec 17, 2024
c1fbe98
chore: added common component for project activity (#6212)
gakshita Dec 17, 2024
c17f43f
- Do not clear temp files that are locked. (#6214)
SatishGandham Dec 17, 2024
fa4af3c
fix: labels empty state for drop down (#6216)
mathalav55 Dec 17, 2024
00f5a5c
refactor: remove cn helper function from the editor package (#6217)
aaryan610 Dec 18, 2024
7002474
Merge branch 'preview' of github.com:makeplane/plane into feat-langua…
mathalav55 Dec 18, 2024
ed5ff98
Merge branch 'preview' of github.com:makeplane/plane into feat-langua…
mathalav55 Dec 18, 2024
fdcc233
* feat: added language support to issue create modal in sidebar
mathalav55 Dec 18, 2024
fd5b829
* fix: added missing translations
mathalav55 Dec 18, 2024
7c4d548
fix: fixed spanish translation
mathalav55 Dec 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/constants/src/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const ORGANIZATION_SIZE = [
"Just myself",
"Just myself", // TODO: translate
"2-10",
"11-50",
"51-200",
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/*
dist/*
out/*
9 changes: 9 additions & 0 deletions packages/i18n/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
4 changes: 4 additions & 0 deletions packages/i18n/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.turbo
out/
dist/
build/
5 changes: 5 additions & 0 deletions packages/i18n/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
20 changes: 20 additions & 0 deletions packages/i18n/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@plane/i18n",
"version": "0.24.1",
"description": "I18n shared across multiple apps internally",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"@plane/utils": "*"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"typescript": "^5.3.3"
}
}
29 changes: 29 additions & 0 deletions packages/i18n/src/components/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { createContext, useEffect } from "react";
import { observer } from "mobx-react";
import { TranslationStore } from "./store";
import { Language, languages } from "../config";

// Create the store instance
const translationStore = new TranslationStore();

// Create Context
export const TranslationContext = createContext<TranslationStore>(translationStore);

export const TranslationProvider = observer(({ children }: any) => {
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add proper TypeScript types for props.

The component uses any type for children props, which bypasses TypeScript's type checking benefits.

-export const TranslationProvider = observer(({ children }: any) => {
+export const TranslationProvider = observer(({ children }: { children: React.ReactNode }) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const TranslationProvider = observer(({ children }: any) => {
export const TranslationProvider = observer(({ children }: { children: React.ReactNode }) => {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mathalav55 check this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

// Handle storage events for cross-tab synchronization
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === "userLanguage" && event.newValue) {
const newLang = event.newValue as Language;
if (languages.includes(newLang)) {
translationStore.setLanguage(newLang);
}
}
};

window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);
Comment on lines +14 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add initial language loading and improve error handling.

The useEffect only handles cross-tab synchronization but doesn't load the initial language from storage. Also, invalid language values should be handled gracefully.

 useEffect(() => {
+  // Load initial language from storage
+  const storedLanguage = window.localStorage.getItem("userLanguage");
+  if (storedLanguage) {
+    try {
+      const initialLang = storedLanguage as Language;
+      if (languages.includes(initialLang)) {
+        translationStore.setLanguage(initialLang);
+      } else {
+        console.warn(`Invalid language value in storage: ${storedLanguage}`);
+      }
+    } catch (error) {
+      console.error("Error loading initial language:", error);
+    }
+  }

   const handleStorageChange = (event: StorageEvent) => {
     if (event.key === "userLanguage" && event.newValue) {
-      const newLang = event.newValue as Language;
-      if (languages.includes(newLang)) {
-        translationStore.setLanguage(newLang);
+      try {
+        const newLang = event.newValue as Language;
+        if (languages.includes(newLang)) {
+          translationStore.setLanguage(newLang);
+        } else {
+          console.warn(`Invalid language value in storage event: ${event.newValue}`);
+        }
+      } catch (error) {
+        console.error("Error processing language change:", error);
       }
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === "userLanguage" && event.newValue) {
const newLang = event.newValue as Language;
if (languages.includes(newLang)) {
translationStore.setLanguage(newLang);
}
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);
useEffect(() => {
// Load initial language from storage
const storedLanguage = window.localStorage.getItem("userLanguage");
if (storedLanguage) {
try {
const initialLang = storedLanguage as Language;
if (languages.includes(initialLang)) {
translationStore.setLanguage(initialLang);
} else {
console.warn(`Invalid language value in storage: ${storedLanguage}`);
}
} catch (error) {
console.error("Error loading initial language:", error);
}
}
const handleStorageChange = (event: StorageEvent) => {
if (event.key === "userLanguage" && event.newValue) {
try {
const newLang = event.newValue as Language;
if (languages.includes(newLang)) {
translationStore.setLanguage(newLang);
} else {
console.warn(`Invalid language value in storage event: ${event.newValue}`);
}
} catch (error) {
console.error("Error processing language change:", error);
}
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);


return <TranslationContext.Provider value={translationStore}>{children}</TranslationContext.Provider>;
});
38 changes: 38 additions & 0 deletions packages/i18n/src/components/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { makeObservable, observable } from "mobx";
import { Language, fallbackLng, languages, translations } from "../config";

export class TranslationStore {
currentLocale: Language = fallbackLng;

constructor() {
makeObservable(this, {
currentLocale: observable.ref,
});
this.initializeLanguage();
}

get availableLanguages() {
return languages;
}

t(key: string) {
return translations[this.currentLocale]?.[key] || translations[fallbackLng][key] || key;
}
Comment on lines +18 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling and logging for missing translations.

The current implementation silently falls back to the key when translations are missing. Consider:

  1. Adding error logging for missing translations in development
  2. Implementing validation for translation completeness

Example implementation:

 t(key: string) {
+  const translation = translations[this.currentLocale]?.[key] || translations[fallbackLng][key];
+  if (!translation && process.env.NODE_ENV === 'development') {
+    console.warn(`Missing translation for key: ${key} in locale: ${this.currentLocale}`);
+  }
-  return translations[this.currentLocale]?.[key] || translations[fallbackLng][key] || key;
+  return translation || key;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
t(key: string) {
return translations[this.currentLocale]?.[key] || translations[fallbackLng][key] || key;
}
t(key: string) {
const translation = translations[this.currentLocale]?.[key] || translations[fallbackLng][key];
if (!translation && process.env.NODE_ENV === 'development') {
console.warn(`Missing translation for key: ${key} in locale: ${this.currentLocale}`);
}
return translation || key;
}


setLanguage(lng: Language) {
localStorage.setItem("userLanguage", lng);
this.currentLocale = lng;
}
Comment on lines +22 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for localStorage operations.

Direct localStorage access could throw in various scenarios (quota exceeded, private browsing, etc.).

Apply this fix:

 setLanguage(lng: Language) {
-  localStorage.setItem("userLanguage", lng);
+  try {
+    localStorage.setItem("userLanguage", lng);
+  } catch (error) {
+    console.error('Failed to save language preference:', error);
+  }
   this.currentLocale = lng;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setLanguage(lng: Language) {
localStorage.setItem("userLanguage", lng);
this.currentLocale = lng;
}
setLanguage(lng: Language) {
try {
localStorage.setItem("userLanguage", lng);
} catch (error) {
console.error('Failed to save language preference:', error);
}
this.currentLocale = lng;
}


initializeLanguage() {
if (typeof window === "undefined") return;
const savedLocale = localStorage.getItem("userLanguage") as Language;
if (savedLocale && languages.includes(savedLocale)) {
this.setLanguage(savedLocale);
} else {
const browserLang = navigator.language.split("-")[0] as Language;
const newLocale = languages.includes(browserLang as Language) ? (browserLang as Language) : fallbackLng;
this.setLanguage(newLocale);
}
}
Comment on lines +27 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider edge cases in language initialization.

The language initialization logic could be more robust:

  1. Handle invalid navigator.language values
  2. Add loading state for async operations
  3. Validate browser language more thoroughly

Suggested implementation:

interface LanguageState {
  isLoading: boolean;
  error: Error | null;
}

initializeLanguage = async () => {
  if (typeof window === "undefined") return;
  
  try {
    this.isLoading = true;
    const savedLocale = localStorage.getItem("userLanguage") as Language;
    
    if (savedLocale && languages.includes(savedLocale)) {
      this.setLanguage(savedLocale);
      return;
    }
    
    const [browserMainLang] = navigator.language.toLowerCase().split("-");
    const newLocale = languages.includes(browserMainLang as Language) 
      ? (browserMainLang as Language) 
      : fallbackLng;
    
    this.setLanguage(newLocale);
  } catch (error) {
    console.error('Language initialization failed:', error);
    this.setLanguage(fallbackLng);
  } finally {
    this.isLoading = false;
  }
}

}
39 changes: 39 additions & 0 deletions packages/i18n/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import en from "../locales/en/translations.json";
import fr from "../locales/fr/translations.json";
import es from "../locales/es/translations.json";
import ja from "../locales/ja/translations.json";

export type Language = (typeof languages)[number];
export type Translations = {
[key: string]: {
[key: string]: string;
};
};
sriramveeraghanta marked this conversation as resolved.
Show resolved Hide resolved

export const fallbackLng = "en";
export const languages = ["en", "fr", "es", "ja"] as const;
export const translations: Translations = {
en,
fr,
es,
ja,
};

export const SUPPORTED_LANGUAGES = [
{
label: "English",
value: "en",
},
{
label: "French",
value: "fr",
},
{
label: "Spanish",
value: "es",
},
{
label: "Japanese",
value: "ja",
},
];
1 change: 1 addition & 0 deletions packages/i18n/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./use-translation";
17 changes: 17 additions & 0 deletions packages/i18n/src/hooks/use-translation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useContext } from "react";
import { TranslationContext } from "../components";
import { Language } from "../config";

export function useTranslation() {
const store = useContext(TranslationContext);
if (!store) {
throw new Error("useTranslation must be used within a TranslationProvider");
}

return {
t: (key: string) => store.t(key),
currentLocale: store.currentLocale,
changeLanguage: (lng: Language) => store.setLanguage(lng),
languages: store.availableLanguages,
};
sriramveeraghanta marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 3 additions & 0 deletions packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./config";
export * from "./components";
export * from "./hooks";
Loading
Loading