Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps/web/lib
diff options
context:
space:
mode:
authorLeitwolf112026-10-03 15:42:08 +0300
committerLeitwolf112026-10-03 15:42:08 +0300
commit549b164464855ae3605c03b58497f91d26555418 (patch)
tree829df551495a11df26b1ebd4cd877db3297deaae /apps/web/lib
parent715953aeaa98ba977e694e8897ccd40151ee0ac0 (diff)
parent282b015e23474c3bb78fe303af627248f5bd2d7a (diff)
downloadprogmaweb-ci/docfx.tar.gz
Merge remote-tracking branch 'origin/main' into ci/docfxci/docfx
Diffstat (limited to 'apps/web/lib')
-rw-r--r--apps/web/lib/locale.server.ts20
-rw-r--r--apps/web/lib/localization.ts376
2 files changed, 392 insertions, 4 deletions
diff --git a/apps/web/lib/locale.server.ts b/apps/web/lib/locale.server.ts
index 590bf2d..977e16b 100644
--- a/apps/web/lib/locale.server.ts
+++ b/apps/web/lib/locale.server.ts
@@ -2,12 +2,26 @@
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1
import "server-only";
-import { cookies } from "next/headers";
-import { isLocale, localeCookieName, type Locale } from "./localization";
+import { cookies, headers } from "next/headers";
+import {
+ isLocale,
+ localeCookieName,
+ negotiateLocale,
+ type Locale,
+} from "./localization";
export { localeCookieName };
+/**
+ * The language of the current request.
+ *
+ * A language the visitor chose is stored in a cookie and always wins. Without
+ * that choice the page follows the language preference of the browser.
+ */
export async function getLocale(): Promise<Locale> {
const value = (await cookies()).get(localeCookieName)?.value;
- return isLocale(value) ? value : "en-US";
+ if (isLocale(value)) {
+ return value;
+ }
+ return negotiateLocale((await headers()).get("accept-language"));
}
diff --git a/apps/web/lib/localization.ts b/apps/web/lib/localization.ts
index 7410520..3ef8460 100644
--- a/apps/web/lib/localization.ts
+++ b/apps/web/lib/localization.ts
@@ -1,9 +1,12 @@
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1
-export const supportedLocales = ["en-US", "de-DE", "ru-RU"] as const;
+export const supportedLocales = ["en-US", "de-DE", "ru-RU", "he-IL"] as const;
export type Locale = (typeof supportedLocales)[number];
+/** The language of a visitor whose browser asks for none that is published. */
+export const fallbackLocale: Locale = "en-US";
+
export function isLocale(value: string | undefined): value is Locale {
return supportedLocales.includes(value as Locale);
}
@@ -13,8 +16,75 @@ export const localeNames: Record<Locale, string> = {
"en-US": "English",
"de-DE": "Deutsch",
"ru-RU": "Русский",
+ "he-IL": "עברית",
};
+/**
+ * The direction in which the text of a language runs.
+ *
+ * Only text follows it. The page layout, the navigation and code samples keep
+ * their left-to-right arrangement in every language.
+ */
+export function textDirection(locale: Locale): "ltr" | "rtl" {
+ return locale === "he-IL" ? "rtl" : "ltr";
+}
+
+/** The arrow that points onward in the reading direction of a language. */
+export function forwardArrow(locale: Locale): string {
+ return textDirection(locale) === "rtl" ? "←" : "→";
+}
+
+// Primary language subtags and the published locale each one selects. `iw` is
+// the former code for Hebrew, which some browsers still send.
+const localeByLanguage = new Map<string, Locale>([
+ ["en", "en-US"],
+ ["de", "de-DE"],
+ ["ru", "ru-RU"],
+ ["he", "he-IL"],
+ ["iw", "he-IL"],
+]);
+
+/**
+ * Chooses the published locale a browser prefers.
+ *
+ * The argument is an `Accept-Language` header: a list of language ranges,
+ * each with an optional quality. The range with the highest quality whose
+ * primary language is published wins, and the order of the header breaks
+ * ties. A missing header, a header without a published language and a range
+ * the visitor refuses with quality zero all give the fallback locale. The
+ * header comes from the browser and is not trusted, so only its beginning is
+ * read.
+ */
+export function negotiateLocale(acceptLanguage: string | null): Locale {
+ if (!acceptLanguage) {
+ return fallbackLocale;
+ }
+ let selected: Locale = fallbackLocale;
+ let selectedQuality = 0;
+ for (const range of acceptLanguage.slice(0, 1024).split(",")) {
+ const [tag = "", ...parameters] = range.trim().split(";");
+ const language = tag.trim().split("-", 1)[0] ?? "";
+ const locale = localeByLanguage.get(language.toLowerCase());
+ if (!locale) {
+ continue;
+ }
+ let quality = 1;
+ for (const parameter of parameters) {
+ const [name, value] = parameter.trim().split("=");
+ if (name === "q") {
+ quality = /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/.test(value ?? "")
+ ? Number(value)
+ : 0;
+ }
+ }
+ if (quality > selectedQuality) {
+ selected = locale;
+ selectedQuality = quality;
+ }
+ }
+ return selected;
+}
+
export const localeCookieName = "progmasoft_locale";
export interface LocaleCookieOptions {
@@ -1055,6 +1125,310 @@ export const messages = {
footerClosing: "Реестр пакетов ViGet от Progmasoft",
},
},
+ "he-IL": {
+ metadata: {
+ homeTitle: "Progmasoft",
+ homeDescription:
+ "Progmasoft בונה מערכות לשפות תכנות, לניהול חבילות ולכלי פיתוח.",
+ accountTitle: "חשבון",
+ accountDescription: "גישה לחשבון Progmasoft שלך וניהולו.",
+ loginTitle: "כניסה",
+ registerTitle: "יצירת חשבון",
+ recoveryTitle: "שחזור החשבון",
+ recoveryDescription: "קבלת עזרה בשחזור הגישה לחשבון Progmasoft.",
+ dashboardTitle: "לוח בקרה",
+ vigetTitle: "מאגר החבילות ViGet מבית Progmasoft",
+ vigetDescription:
+ "מאגר החבילות של Visual X#‎ פעיל, אך עדיין לא פורסמו בו חבילות.",
+ dslPluginsTitle: "תוספי DSL · מאגר החבילות ViGet מבית Progmasoft",
+ dslPluginsDescription:
+ "קטלוג תוספי Kotlin DSL של ViGet פעיל, אך עדיין לא פורסמו בו תוספים.",
+ },
+ navigation: {
+ primary: "ניווט ראשי",
+ organization: "הארגון",
+ products: "מוצרים",
+ principles: "עקרונות",
+ account: "חשבון",
+ dashboard: "לוח בקרה",
+ signIn: "כניסה",
+ createAccount: "יצירת חשבון",
+ theme: "ערכת נושא",
+ light: "בהיר",
+ dark: "כהה",
+ language: "שפה",
+ },
+ footer: {
+ summary: "תשתית לשפות תכנות ומערכות למפתחים, הבנויות על חוזים יציבים.",
+ products: "מוצרים",
+ openSource: "קוד פתוח",
+ organization: "הארגון",
+ support: "תמיכה",
+ websiteSource: "קוד המקור של האתר",
+ closing: "מתוכנן לבהירות, לאבטחה ולתחזוקה ארוכת טווח.",
+ },
+ home: {
+ heroEyebrow: "מערכות למפתחים מבית Progmasoft",
+ heroTitleStart: "כלים צריכים להפוך עבודה קשה",
+ heroTitleAccent: "למובנת.",
+ heroDescription:
+ "אנחנו בונים תשתית לשפות תכנות, מערכות חבילות וכלי פיתוח סביב חוזים מפורשים במקום מורכבות מקרית.",
+ exploreProducts: "למוצרים",
+ browseSource: "לעיון בקוד המקור",
+ facts: [
+ ["פתוח", "הנדסה גלויה לציבור"],
+ ["מבוסס טיפוסים", "חוזים לפני קיצורי דרך"],
+ ["קוד מכונה", "ביצועים בלי מסתורין"],
+ ],
+ visualLabel: "עקרונות המוצר של Progmasoft",
+ stack: [
+ ["מוצרים", "חוויות ממוקדות", "מטרה ברורה · שמות יציבים · זהות ציבורית"],
+ ["פלטפורמה", "בסיס חשבון משותף", "אימות · גישה לשירותים · שחזור"],
+ [
+ "תפעול",
+ "תשתית בבעלותנו",
+ "ניתנת לניטור · ניתנת לתחזוקה · בהפעלה ישירה",
+ ],
+ ],
+ productsEyebrow: "מוצרים ופרויקטים",
+ productsTitle: "אקוסיסטם אחד, גבולות ברורים.",
+ productsDescription: "לכל רכיב יש אחריות אחת וחוזה מתועד מול השכבה הבאה.",
+ products: [
+ [
+ "שפת תכנות",
+ "Visual X#",
+ "שפת תכנות מודרנית שמפתחת Progmasoft, עם אתר מוצר ותיעוד ייעודי משלה.",
+ "להכיר את Visual X#‎",
+ ],
+ [
+ "מאגר חבילות",
+ "ViGet",
+ "המאגר הרשמי של חבילות ותוספי DSL לאקוסיסטם של Visual X#‎, בהפעלה ישירה של Progmasoft.",
+ "לפתיחת ViGet",
+ ],
+ [
+ "כלי פיתוח",
+ "הנדסה פתוחה",
+ "מהדר, מעצב קוד, linter, מנתח, מערכת פרויקטים ועורך, המפותחים במאגרים ציבוריים.",
+ "לצפייה ב-GitHub",
+ ],
+ ],
+ principlesEyebrow: "עקרונות הנדסיים",
+ principlesTitle: "בנוי כדי להישאר קריא.",
+ principlesDescription:
+ "ארכיטקטורה מועילה רק כאשר תורם חדש יכול להבין לאן שייכת החלטה וכיצד לאמת אותה.",
+ principles: [
+ [
+ "חוזים מפורשים",
+ "גבולות מבוססי טיפוסים חושפים בעלות, תאימות והתנהגות בעת כשל.",
+ ],
+ [
+ "אימות אמיתי",
+ "הבדיקות מפעילות תוצרים מותקנים, נתיבים דמויי סביבת ייצור והתנהגות נצפית.",
+ ],
+ [
+ "שמות יציבים",
+ "אוצר המילים הציבורי נגזר ממודל המוצר ולא משאריות היסטוריות של המימוש.",
+ ],
+ ],
+ accountEyebrow: "חשבון Progmasoft",
+ accountTitle: "זהות אחת לשירותי Progmasoft.",
+ accountDescription:
+ "ניהול הפרופיל והגישה העתידית לשירותים מאזור החשבון הייעודי.",
+ openAccount: "לחשבון",
+ },
+ account: {
+ securityLabel: "מאפייני האבטחה של החשבון",
+ trust: [
+ "עוגיית הפעלה מאובטחת",
+ "גיבוב סיסמאות בצד השרת",
+ "אימות עם הגבלת קצב",
+ ],
+ landingEyebrow: "חשבון Progmasoft",
+ landingTitle: "זהות ברורה אחת לכל שירות נתמך.",
+ landingDescription:
+ "החשבון שומר את הפרופיל, האימות והגישה לשירותים בתוך גבול אבטחה ייעודי.",
+ identity: [
+ ["שם החשבון", "הזהות הציבורית היציבה שלך ושם המפרסם שלך ב-ViGet"],
+ ["דוא״ל", "שחזור והודעות אבטחה"],
+ ["הפעלה", "גישה מאובטחת מהדפדפן שניתן לבטל"],
+ ],
+ features: [
+ [
+ "אבטחה מכוונת",
+ "הסיסמאות מגובבות בצד השרת, ואסימוני ההפעלה נשמרים כתקצירים בלבד.",
+ ],
+ [
+ "שמות צפויים",
+ "שמות חשבון קנוניים מונעים כתובות דו־משמעיות והתחזות שמבוססת על הבדלי אותיות גדולות וקטנות בלבד. ViGet משתמש בשם הזה בדיוק כשם מפרסם החבילה ואינו יוצר זהות שנייה.",
+ ],
+ [
+ "גבולות בין שירותים",
+ "המוצרים מבקשים גישה מפורשת לחשבון במקום לשתף מצב יישום נסתר.",
+ ],
+ ],
+ loginEyebrow: "חשבון Progmasoft",
+ loginTitle: "כניסה מאובטחת.",
+ loginDescription: "המשך ללוח הבקרה של החשבון ולשירותים המקושרים.",
+ registerEyebrow: "יצירת זהות",
+ registerTitle: "מתחילים עם חשבון יציב.",
+ registerDescription: "בחירת השם שישמש בכתובת חשבון Progmasoft שלך.",
+ recoveryEyebrow: "תמיכה בחשבון",
+ recoveryTitle: "שחזור החשבון.",
+ recoveryDescription:
+ "כשאי אפשר עוד להיכנס, יש לפנות בערוץ התמיכה המאומת.",
+ recoveryPanelEyebrow: "שחזור חשבון",
+ recoveryPanelTitle: "שחזור גישה בבטחה",
+ recoveryPanelDescription:
+ "שחזור סיסמה אוטומטי אינו זמין בשלב ההשקה הראשוני של מערכת החשבונות. יש לפנות לתמיכה של Progmasoft מכתובת הדוא״ל הרשומה בחשבון, כדי שניתן יהיה לאמת את הבעלות.",
+ recoveryEmailSubject: "שחזור חשבון Progmasoft",
+ contactSupport: "פנייה לתמיכה",
+ rememberedPassword: "נזכרת בסיסמה?",
+ returnToSignIn: "חזרה לכניסה",
+ },
+ auth: {
+ createHeading: "יצירת החשבון שלך",
+ loginHeading: "ברוכים השבים",
+ createDescription: "זהות Progmasoft אחת לכל השירותים הנתמכים.",
+ loginDescription: "כניסה עם כתובת הדוא״ל המשויכת לחשבון Progmasoft שלך.",
+ accountName: "שם החשבון",
+ accountHint:
+ "8 עד 128 אותיות ASCII או ספרות, כשהתו הראשון הוא אות גדולה.",
+ email: "כתובת דוא״ל",
+ password: "סיסמה",
+ forgotPassword: "שכחת את הסיסמה?",
+ passwordHint: "לפחות 12 תווים. יש תמיכה במשפטי סיסמה ארוכים.",
+ confirmPassword: "אימות הסיסמה",
+ show: "הצגה",
+ hide: "הסתרה",
+ working: "בתהליך…",
+ create: "יצירת חשבון",
+ signIn: "כניסה",
+ or: "או",
+ google: "המשך עם Google",
+ already: "כבר יש לך חשבון?",
+ newUser: "פעם ראשונה ב-Progmasoft?",
+ createLink: "יצירת חשבון",
+ mismatch: "אימות הסיסמה אינו תואם.",
+ unreachable: "שירות החשבונות אינו זמין זמנית. יש לנסות שוב בקרוב.",
+ missingName: "השרת לא החזיר שם חשבון.",
+ unreadableName: "לא ניתן היה לקרוא את השדה ״שם החשבון״.",
+ requestFailed: "לא ניתן היה להשלים את הבקשה.",
+ googleSignInFailed:
+ "לא ניתן היה להשלים את הכניסה עם Google. יש לנסות שוב.",
+ googleAccountNameRequired: "יש לבחור שם חשבון לפני ההמשך עם Google.",
+ googleAccountNameUnavailable: "שם החשבון הזה אינו זמין. יש לבחור שם אחר.",
+ googleInvalidAccountName: "יש להזין שם חשבון תקין לפני ההמשך עם Google.",
+ },
+ dashboard: {
+ loading: "החשבון נטען…",
+ loadFailed: "לא ניתן היה לטעון את לוח הבקרה.",
+ unreachable: "שירות החשבונות אינו זמין זמנית.",
+ navigation: "ניווט בלוח הבקרה",
+ overview: "סקירה",
+ security: "אבטחה",
+ services: "שירותים",
+ signOut: "יציאה",
+ eyebrow: "סקירת החשבון",
+ welcome: "ברוכים הבאים",
+ description: "ניהול הזהות שמשמשת את שירותי Progmasoft.",
+ profile: "פרופיל",
+ identity: "זהות החשבון",
+ accountName: "שם החשבון",
+ publisherName: "שם המפרסם ב-ViGet",
+ email: "דוא״ל",
+ created: "נוצר",
+ passwordSessions: "סיסמה והפעלות",
+ securityDescription:
+ "הדפדפן שלך משתמש בעוגיית הפעלה מאובטחת מסוג HttpOnly. סודות ההפעלה לעולם אינם נשמרים כטקסט גלוי.",
+ changePassword: "שינוי סיסמה",
+ products: "מוצרי Progmasoft מקושרים",
+ publisherPrefix: "שם המפרסם",
+ sameAccount: "שם החשבון שלך",
+ profileDescription: "פרופיל באקוסיסטם המפתחים",
+ planned: "מתוכנן",
+ },
+ viget: {
+ navigationLabel: "ניווט במאגר",
+ packages: "חבילות",
+ dslPlugins: "תוספי DSL",
+ account: "חשבון",
+ login: "כניסה",
+ register: "הרשמה",
+ homeEyebrow: "מאגר החבילות ViGet",
+ homeTitle: "חבילות לאקוסיסטם של Visual X#‎.",
+ homeDescription:
+ "ViGet הוא המקור הרשמי לחבילות Visual X#‎ ולתוספי DSL לפרויקטים, בהפעלה ישירה של Progmasoft.",
+ createAccount: "יצירת חשבון",
+ exploreVisualXSharp: "להכיר את Visual X#‎",
+ available: "המאגר זמין",
+ emptyTitle: "הקטלוג הציבורי ריק.",
+ emptyDescription:
+ "עדיין לא פורסמו גרסאות של חבילות. ViGet יציג כאן חבילות אמיתיות כשיהיו זמינות.",
+ packageFormat: "תבנית החבילה",
+ publisherIdentity: "זהות המפרסם",
+ publishing: "פרסום",
+ publishingClosed: "עדיין לא נפתח",
+ catalogsEyebrow: "קטלוגים",
+ catalogsTitle: "שני סוגי תוצרים, מאגר אחד.",
+ catalogsDescription:
+ "לחבילות ולתוספי DSL לפרויקטים יש מרחבי קואורדינטות נפרדים וצפויים.",
+ visualPackages: "חבילות Visual X#‎",
+ vipkgCatalog: "קטלוג ViPkg",
+ vipkgDescription:
+ "ספריות ויישומים שנכתבו ב-Visual X#‎ ומופצים כתוצרי ‎.vipkg.",
+ projectExtensions: "הרחבות לפרויקטים",
+ kotlinPlugins: "תוספי Kotlin DSL",
+ kotlinDescription:
+ "תוספי Kotlin בתבנית JAR שמרחיבים את תצורת הפרויקט Visual.XSharp.kts.",
+ openCatalog: "לפתיחת הקטלוג",
+ contractEyebrow: "חוזה המאגר",
+ contractTitle: "בעלות ברורה מהזהות ועד לתוצר.",
+ principles: [
+ [
+ "שם חשבון אחד",
+ "שם חשבון Progmasoft שלך הוא גם שם המפרסם שלך ב-ViGet.",
+ ],
+ [
+ "קואורדינטות תלויות רישיות",
+ "שמות המפרסם והחבילה שומרים על האיות הציבורי המדויק שלהם.",
+ ],
+ ["בלי גרסאות דמה", "הקטלוג נשאר ריק ביושר עד שמתפרסם תוצר חתום אמיתי."],
+ ],
+ pluginEyebrow: "ViGet · תוספי Kotlin DSL",
+ pluginTitle: "להרחיב את מודל הפרויקט.",
+ pluginDescription:
+ "תוספי Kotlin DSL הם תוצרי JAR עבור Visual.XSharp.kts. הם נפרדים מחבילות ‎.vipkg של Visual X#‎.",
+ backToPackages: "חזרה לחבילות",
+ followDevelopment: "למעקב אחר הפיתוח",
+ pluginAvailable: "הקטלוג זמין",
+ pluginEmptyTitle: "עדיין אין תוספי DSL ציבוריים.",
+ pluginEmptyDescription: "הקטלוג מוכן, ובכוונה אינו מכיל תוצרי דמה.",
+ guidance: [
+ [
+ "תבנית",
+ "Kotlin JAR",
+ "תוספי DSL פועלים כהרחבות פרויקט של Kotlin/JVM.",
+ ],
+ [
+ "מיקום",
+ "קטלוג ייעודי",
+ "קואורדינטות של תוסף מתחילות תמיד ב-‎/dslplugins.",
+ ],
+ [
+ "זמינות",
+ "הפרסום סגור",
+ "הפרסום ייפתח רק לאחר השלמת חוזה התוספים החתומים.",
+ ],
+ ],
+ footerDescription:
+ "ViGet הוא מאגר החבילות ותוספי Kotlin DSL של האקוסיסטם של Visual X#‎.",
+ registry: "מאגר",
+ support: "תמיכה",
+ source: "קוד מקור",
+ footerClosing: "מאגר החבילות ViGet מבית Progmasoft",
+ },
+ },
} as const;
export function getMessages(locale: Locale) {