Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eb15869
fix(pdf): support RTL text (Persian/Arabic) in PDF export
claude Jun 1, 2026
b75c454
Merge pull request #1 from danialshirali16/claude/zealous-carson-Zogxm
danialshirali16 Jun 1, 2026
8cd6797
feat(i18n): add Persian (fa) locale — common.json (814 keys)
claude Jun 3, 2026
1825923
feat(i18n): add Persian (fa) locale — workspace-settings.json (429 keys)
claude Jun 3, 2026
bbc5a09
feat(i18n): add Persian (fa) locale — project-settings.json (406 keys)
claude Jun 3, 2026
f51cb60
feat(i18n): add Persian (fa) locale — work-item-type.json (341 keys)
claude Jun 3, 2026
65a67ef
feat(i18n): add Persian (fa) locale — integration.json (310 keys)
claude Jun 3, 2026
0b65fbc
feat(i18n): add Persian (fa) locale — work-item.json (309 keys)
claude Jun 3, 2026
3e6096e
feat(i18n): add Persian (fa) locale — project.json (308 keys)
claude Jun 3, 2026
b51bf27
feat(i18n): add Persian (fa) locale — workspace.json (280 keys)
claude Jun 3, 2026
eb4d5e1
feat(i18n): add Persian (fa) locale — auth.json (276 keys)
claude Jun 3, 2026
bf740d9
feat(i18n): add Persian (fa) locale — template.json (234 keys)
claude Jun 3, 2026
8c985e0
feat(i18n): add Persian (fa) locale — automation.json (206 keys)
claude Jun 3, 2026
9085ff8
feat(i18n): add Persian (fa) locale — empty-state.json (205 keys)
claude Jun 3, 2026
e41d04c
feat(i18n): add Persian (fa) locale — power-k.json (169 keys)
claude Jun 3, 2026
ef7a001
feat(i18n): add Persian (fa) locale — tour.json (141 keys)
claude Jun 3, 2026
38f80ce
feat(i18n): add Persian (fa) locale — settings.json (116 keys)
claude Jun 3, 2026
d85cd72
feat(i18n): add Persian (fa) locale — wiki.json (92 keys)
claude Jun 3, 2026
87a0574
feat(i18n): add Persian (fa) locale — page.json (84 keys)
claude Jun 3, 2026
43aad14
feat(i18n): add Persian (fa) locale — final 11 files (461 keys)
claude Jun 3, 2026
cebfd1a
feat(i18n): register Persian (fa) as a supported language
claude Jun 3, 2026
344b4ff
Merge pull request #2 from danialshirali16/claude/eloquent-wozniak-YWDUc
danialshirali16 Jun 3, 2026
296c7ed
feat(i18n): wire up RTL document direction for Persian
claude Jun 23, 2026
be86965
docs(i18n): clarify how to add new RTL locales
claude Jun 23, 2026
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
27 changes: 21 additions & 6 deletions apps/live/src/lib/pdf/node-renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ const getFlexAlignStyle = (textAlign: string | null | undefined): Style => {
return {};
};

const getRtlStyle = (dir: string | null | undefined): Style => {
if (dir !== "rtl") return {};
return { fontFamily: "Vazirmatn" };
};

export const nodeRenderers: NodeRendererRegistry = {
doc: (_node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => (
<View key={ctx.getKey()}>{children}</View>
Expand All @@ -98,16 +103,21 @@ export const nodeRenderers: NodeRendererRegistry = {

paragraph: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => {
const textAlign = node.attrs?.textAlign as string | null;
const dir = node.attrs?.dir as string | null | undefined;
const isRtl = dir === "rtl";
// For RTL paragraphs with no explicit alignment, default to right-aligned
const effectiveTextAlign = textAlign ?? (isRtl ? "right" : null);
const background = node.attrs?.backgroundColor as string | undefined;
const alignStyle = getTextAlignStyle(textAlign);
const flexStyle = getFlexAlignStyle(textAlign);
const alignStyle = getTextAlignStyle(effectiveTextAlign);
const flexStyle = getFlexAlignStyle(effectiveTextAlign);
const rtlStyle = getRtlStyle(dir);
const resolvedBgColor =
background && background !== "default" ? resolveColorForPdf(background, "background") : null;
const bgStyle = resolvedBgColor ? { backgroundColor: resolvedBgColor } : {};

return (
<View key={ctx.getKey()} style={[pdfStyles.paragraphWrapper, flexStyle, bgStyle]}>
<Text style={[pdfStyles.paragraph, alignStyle, bgStyle]}>{children}</Text>
<Text style={[pdfStyles.paragraph, alignStyle, rtlStyle, bgStyle]}>{children}</Text>
</View>
);
},
Expand All @@ -117,12 +127,17 @@ export const nodeRenderers: NodeRendererRegistry = {
const styleKey = `heading${level}` as keyof typeof pdfStyles;
const style = pdfStyles[styleKey] || pdfStyles.heading1;
const textAlign = node.attrs?.textAlign as string | null;
const alignStyle = getTextAlignStyle(textAlign);
const flexStyle = getFlexAlignStyle(textAlign);
const dir = node.attrs?.dir as string | null | undefined;
const isRtl = dir === "rtl";
// For RTL headings with no explicit alignment, default to right-aligned
const effectiveTextAlign = textAlign ?? (isRtl ? "right" : null);
const alignStyle = getTextAlignStyle(effectiveTextAlign);
const flexStyle = getFlexAlignStyle(effectiveTextAlign);
const rtlStyle = getRtlStyle(dir);

return (
<View key={ctx.getKey()} style={flexStyle}>
<Text style={[style, alignStyle]}>{children}</Text>
<Text style={[style, alignStyle, rtlStyle]}>{children}</Text>
</View>
);
},
Expand Down
19 changes: 19 additions & 0 deletions apps/live/src/lib/pdf/plane-pdf-exporter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ Font.register({
],
});

// Resolve Vazirmatn font files from the fonts directory at the package root.
// Place the woff files at apps/live/fonts/vazirmatn/ before starting the server.
// Download from: https://github.com/rastikerdar/vazirmatn/releases
const vazirmatnFontDir = path.resolve(process.cwd(), "fonts/vazirmatn");

Comment on lines +56 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid process.cwd() for font asset resolution in the live exporter.

This path is launch-directory dependent; running the service from a different working directory can break RTL font loading and PDF rendering. Resolve from the module location instead.

Suggested fix
+import { fileURLToPath } from "node:url";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const vazirmatnFontDir = path.resolve(process.cwd(), "fonts/vazirmatn");
+const vazirmatnFontDir = path.resolve(__dirname, "../../../fonts/vazirmatn");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/live/src/lib/pdf/plane-pdf-exporter.tsx` around lines 56 - 57, Replace
process.cwd() with __dirname in the vazirmatnFontDir path resolution to ensure
the font directory is resolved from the module's location rather than the launch
directory. This prevents font loading failures when the service is started from
different working directories. Change the path.resolve call to use __dirname as
the base path instead of process.cwd().

Font.register({
family: "Vazirmatn",
fonts: [
{
src: path.join(vazirmatnFontDir, "vazirmatn-regular.woff"),
fontWeight: 400,
},
{
src: path.join(vazirmatnFontDir, "vazirmatn-bold.woff"),
fontWeight: 700,
},
],
});

export const createPdfDocument = (doc: TipTapDocument, options: PDFExportOptions = {}) => {
const { title, author, subject, pageSize = "A4", pageOrientation = "portrait", metadata, noAssets } = options;

Expand Down
13 changes: 13 additions & 0 deletions apps/web/core/components/editor/pdf/document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import interSemibold from "@/app/assets/fonts/inter/semibold.ttf?url";
import interThin from "@/app/assets/fonts/inter/thin.ttf?url";
import interUltraBold from "@/app/assets/fonts/inter/ultrabold.ttf?url";
import interUltraLight from "@/app/assets/fonts/inter/ultralight.ttf?url";
// Vazirmatn — Persian/Arabic font for RTL content.
// Place font files at apps/web/app/assets/fonts/vazirmatn/ before building.
// Download from: https://github.com/rastikerdar/vazirmatn/releases
import vazirmatnBold from "@/app/assets/fonts/vazirmatn/bold.ttf?url";
import vazirmatnRegular from "@/app/assets/fonts/vazirmatn/regular.ttf?url";
// constants
import { EDITOR_PDF_DOCUMENT_STYLESHEET } from "@/constants/editor";

Expand Down Expand Up @@ -44,6 +49,14 @@ Font.register({
],
});

Font.register({
family: "Vazirmatn",
fonts: [
{ src: vazirmatnRegular, fontWeight: "normal" },
{ src: vazirmatnBold, fontWeight: "bold" },
],
});

type Props = {
content: string;
pageFormat: PageProps["size"];
Expand Down
6 changes: 6 additions & 0 deletions apps/web/core/constants/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ const EDITOR_PDF_FONT_FAMILY_STYLES: Styles = {
".courier-bold": {
fontFamily: "Courier-Bold",
},
// RTL content (Persian, Arabic, Hebrew, etc.) — use a font that carries
// the required Unicode shaping tables so letters connect correctly.
"[dir='rtl']": {
fontFamily: "Vazirmatn",
textAlign: "right",
},
};

const EDITOR_PDF_TYPOGRAPHY_STYLES: Styles = {
Expand Down
11 changes: 11 additions & 0 deletions packages/i18n/src/constants/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "Română", value: "ro" },
{ label: "Tiếng việt", value: "vi-VN" },
{ label: "Türkçe", value: "tr-TR" },
{ label: "فارسی", value: "fa" },
];

export const LANGUAGE_STORAGE_KEY = "userLanguage";

// Languages that render right-to-left. Used to set the document `dir` attribute
// so the entire UI layout (not just text) mirrors for these locales.
// When adding a new RTL locale (e.g. Arabic "ar", Hebrew "he", Urdu "ur"),
// register it in TLanguage + SUPPORTED_LANGUAGES first, then add it here.
export const RTL_LANGUAGES: TLanguage[] = ["fa"];

export const isRTLLanguage = (lng: TLanguage): boolean => RTL_LANGUAGES.includes(lng);

export const getLanguageDirection = (lng: TLanguage): "rtl" | "ltr" => (isRTLLanguage(lng) ? "rtl" : "ltr");
5 changes: 4 additions & 1 deletion packages/i18n/src/core/set-language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { initPromise, i18nInstance } from "./instance";
import { LANGUAGE_STORAGE_KEY } from "../constants/language";
import { LANGUAGE_STORAGE_KEY, getLanguageDirection } from "../constants/language";
import type { TLanguage } from "../types";

export async function setLanguage(lng: TLanguage): Promise<void> {
Expand All @@ -14,5 +14,8 @@ export async function setLanguage(lng: TLanguage): Promise<void> {
if (typeof window !== "undefined") {
localStorage.setItem(LANGUAGE_STORAGE_KEY, lng);
document.documentElement.lang = lng;
// Mirror the whole UI for RTL locales (e.g. Persian) by setting the
// document direction, not just translating the text.
document.documentElement.dir = getLanguageDirection(lng);
}
}
9 changes: 8 additions & 1 deletion packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,11 @@ export type { TNamespace } from "./constants/namespaces";
export { setLanguage } from "./core/set-language";

// Constants
export { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY } from "./constants/language";
export {
FALLBACK_LANGUAGE,
SUPPORTED_LANGUAGES,
LANGUAGE_STORAGE_KEY,
RTL_LANGUAGES,
isRTLLanguage,
getLanguageDirection,
} from "./constants/language";
34 changes: 34 additions & 0 deletions packages/i18n/src/locales/fa/accessibility.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"aria_labels": {
"projects_sidebar": {
"workspace_logo": "لوگوی فضای کاری",
"open_workspace_switcher": "باز کردن تغییردهنده فضای کاری",
"open_user_menu": "باز کردن منوی کاربر",
"open_command_palette": "باز کردن پالت دستور",
"open_extended_sidebar": "باز کردن نوار کناری گسترده",
"close_extended_sidebar": "بستن نوار کناری گسترده",
"create_favorites_folder": "ایجاد پوشه موردعلاقه‌ها",
"open_folder": "باز کردن پوشه",
"close_folder": "بستن پوشه",
"open_favorites_menu": "باز کردن منوی موردعلاقه‌ها",
"close_favorites_menu": "بستن منوی موردعلاقه‌ها",
"enter_folder_name": "وارد کردن نام پوشه",
"create_new_project": "ایجاد پروژه جدید",
"open_projects_menu": "باز کردن منوی پروژه‌ها",
"close_projects_menu": "بستن منوی پروژه‌ها",
"toggle_quick_actions_menu": "نمایش/پنهان کردن منوی عملکردهای سریع",
"open_project_menu": "باز کردن منوی پروژه",
"close_project_menu": "بستن منوی پروژه",
"collapse_sidebar": "جمع کردن نوار کناری",
"expand_sidebar": "گسترش نوار کناری",
"edition_badge": "باز کردن مودال پلن‌های پولی"
},
"auth_forms": {
"clear_email": "پاک کردن ایمیل",
"show_password": "نمایش رمز عبور",
"hide_password": "پنهان کردن رمز عبور",
"close_alert": "بستن هشدار",
"close_popover": "بستن پاپ‌اور"
}
}
}
Loading