Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add proper typing for children prop

Instead of using any, consider using proper React types for the children prop.

-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 }) => {

// 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);
}, []);

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.

🛠️ Refactor suggestion

Enhance translation lookup robustness

The current translation lookup could be improved in several ways:

  1. Add logging for missing translations
  2. Handle nested translation keys
  3. Add type safety for translation keys
-  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;
   }
📝 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;
}

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);
}
}
}
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;
};
};

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,
};
}
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";
49 changes: 49 additions & 0 deletions packages/i18n/src/locales/en/translations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"submit": "Submit",
"cancel": "Cancel",
"loading": "Loading",
"error": "Error",
"success": "Success",
"warning": "Warning",
"info": "Info",
"close": "Close",
"yes": "Yes",
"no": "No",
"ok": "OK",
"name": "Name",
"description": "Description",
"search": "Search",
"add_member": "Add member",
"remove_member": "Remove member",
"add_members": "Add members",
"remove_members": "Remove members",
"add": "Add",
"remove": "Remove",
"add_new": "Add new",
"remove_selected": "Remove selected",
"first_name": "First name",
"last_name": "Last name",
"email": "Email",
"display_name": "Display name",
"role": "Role",
"timezone": "Timezone",
"avatar": "Avatar",
"cover_image": "Cover image",
"password": "Password",
"change_cover": "Change cover",
"language": "Language",
"saving": "Saving...",
"save_changes": "Save changes",
"deactivate_account": "Deactivate account",
"deactivate_account_description": "When deactivating an account, all of the data and resources within that account will be permanently removed and cannot be recovered.",
"profile_settings": "Profile settings",
"your_account": "Your account",
"profile": "Profile",
"security": "Security",
"activity": "Activity",
"appearance": "Appearance",
"notifications": "Notifications",
"workspaces": "Workspaces",
"create_workspace": "Create workspace",
"invitations": "Invitations"
}
49 changes: 49 additions & 0 deletions packages/i18n/src/locales/es/translations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"submit": "Enviar",
"cancel": "Cancelar",
"loading": "Cargando",
"error": "Error",
"success": "Éxito",
"warning": "Advertencia",
"info": "Información",
"close": "Cerrar",
"yes": "Sí",
"no": "No",
"ok": "OK",
"name": "Nombre",
"description": "Descripción",
"search": "Buscar",
"add_member": "Agregar miembro",
"remove_member": "Eliminar miembro",
"add_members": "Agregar miembros",
"remove_members": "Eliminar miembros",
"add": "Agregar",
"remove": "Eliminar",
"add_new": "Agregar nuevo",
"remove_selected": "Eliminar seleccionados",
"first_name": "Nombre",
"last_name": "Apellido",
"email": "Correo electrónico",
"display_name": "Nombre para mostrar",
"role": "Rol",
"timezone": "Zona horaria",
"avatar": "Avatar",
"cover_image": "Imagen de portada",
"password": "Contraseña",
"change_cover": "Cambiar portada",
"language": "Idioma",
"saving": "Guardando...",
"save_changes": "Guardar cambios",
"deactivate_account": "Desactivar cuenta",
"deactivate_account_description": "Al desactivar una cuenta, todos los datos y recursos dentro de esa cuenta se eliminarán permanentemente y no se podrán recuperar.",
"profile_settings": "Configuración de perfil",
"your_account": "Tu cuenta",
"profile": "Perfil",
"security": "Seguridad",
"activity": "Actividad",
"appearance": "Apariencia",
"notifications": "Notificaciones",
"workspaces": "Workspaces",
"create_workspace": "Crear workspace",
"invitations": "Invitaciones"
}
49 changes: 49 additions & 0 deletions packages/i18n/src/locales/fr/translations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"submit": "Soumettre",
"cancel": "Annuler",
"loading": "Chargement",
"error": "Erreur",
"success": "Succès",
"warning": "Avertissement",
"info": "Info",
"close": "Fermer",
"yes": "Oui",
"no": "Non",
"ok": "OK",
"name": "Nom",
"description": "Description",
"search": "Rechercher",
"add_member": "Ajouter un membre",
"remove_member": "Supprimer un membre",
"add_members": "Ajouter des membres",
"remove_members": "Supprimer des membres",
"add": "Ajouter",
"remove": "Supprimer",
"add_new": "Ajouter nouveau",
"remove_selected": "Supprimer la sélection",
"first_name": "Prénom",
"last_name": "Nom de famille",
"email": "Email",
"display_name": "Nom d'affichage",
"role": "Rôle",
"timezone": "Fuseau horaire",
"avatar": "Avatar",
"cover_image": "Image de couverture",
"password": "Mot de passe",
"change_cover": "Modifier la couverture",
"language": "Langue",
"saving": "Enregistrement...",
"save_changes": "Enregistrer les modifications",
"deactivate_account": "Désactiver le compte",
"deactivate_account_description": "Lors de la désactivation d'un compte, toutes les données et ressources de ce compte seront définitivement supprimées et ne pourront pas être récupérées.",
"profile_settings": "Paramètres du profil",
"your_account": "Votre compte",
"profile": "Profil",
"security": " Sécurité",
"activity": "Activité",
"appearance": "Apparence",
"notifications": "Notifications",
"workspaces": "Workspaces",
"create_workspace": "Créer un workspace",
"invitations": "Invitations"
}
Comment on lines +1 to +49
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 missing label-related translations.

Given that this PR adds functionality for label management, please add translations for label-related terms such as:

  • "Add label"
  • "Create label"
  • "Label name"
  • "Label color"
  • "Delete label"

Also, consider these improvements:

  • Line 38: The account deactivation message could be more concise in French
  • Line 42: Remove extra space in "Sécurité"

49 changes: 49 additions & 0 deletions packages/i18n/src/locales/ja/translations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"submit": "送信",
"cancel": "キャンセル",
"loading": "読み込み中",
"error": "エラー",
"success": "成功",
"warning": "警告",
"info": "情報",
"close": "閉じる",
"yes": "はい",
"no": "いいえ",
"ok": "OK",
"name": "名前",
"description": "説明",
"search": "検索",
"add_member": "メンバーを追加",
"remove_member": "メンバーを削除",
"add_members": "メンバーを追加",
"remove_members": "メンバーを削除",
"add": "追加",
"remove": "削除",
"add_new": "新規追加",
"remove_selected": "選択項目を削除",
"first_name": "名",
"last_name": "姓",
"email": "メールアドレス",
"display_name": "表示名",
"role": "役割",
"timezone": "タイムゾーン",
"avatar": "アバター",
"cover_image": "カバー画像",
"password": "パスワード",
"change_cover": "カバーを変更",
"language": "言語",
"saving": "保存中...",
"save_changes": "変更を保存",
"deactivate_account": "アカウントを無効化",
"deactivate_account_description": "アカウントを無効化すると、そのアカウント内のすべてのデータとリソースが完全に削除され、復元することはできません。",
"profile_settings": "プロフィール設定",
"your_account": "アカウント",
"profile": "プロフィール",
"security": "セキュリティ",
"activity": "アクティビティ",
"appearance": "アピアンス",
"notifications": "通知",
"workspaces": "ワークスペース",
"create_workspace": "ワークスペースを作成",
"invitations": "招待"
}
10 changes: 10 additions & 0 deletions packages/i18n/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "@plane/typescript-config/react-library.json",
"compilerOptions": {
"jsx": "react",
"lib": ["esnext", "dom"],
"resolveJsonModule": true
},
"include": ["./src"],
"exclude": ["dist", "build", "node_modules"]
}
Loading