diff --git a/package/src/i18n/ar.json b/package/src/i18n/ar.json new file mode 100644 index 0000000000..459afbde3b --- /dev/null +++ b/package/src/i18n/ar.json @@ -0,0 +1,230 @@ +{ + "+{{count}}_few": "+{{count}}", + "+{{count}}_many": "+{{count}}", + "+{{count}}_one": "+{{count}}", + "+{{count}}_other": "+{{count}}", + "+{{count}}_two": "+{{count}}", + "+{{count}}_zero": "+{{count}}", + "1 Reply": "رد واحد", + "1 Thread Reply": "رد واحد في الموضوع", + "Add a comment": "أضف تعليقاً", + "Add an option": "أضف خياراً", + "Allow access to your Gallery": "اسمح بالوصول إلى المعرض", + "Allow camera access in device settings": "اسمح بالوصول إلى الكاميرا في إعدادات الجهاز", + "Also send to channel": "أرسل أيضاً إلى القناة", + "Anonymous": "مجهول", + "Anonymous voting": "تصويت مجهول", + "Are you sure you want to permanently delete this message?": "هل أنت متأكد أنك تريد حذف هذه الرسالة نهائياً؟", + "Are you sure?": "هل أنت متأكد؟", + "Ask a question": "اطرح سؤالاً", + "Ban User": "منع المستخدم", + "Block User": "حظر المستخدم", + "Cancel": "إلغاء", + "Cannot Flag Message": "تعذر الإبلاغ عن الرسالة", + "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "فكّر في تأثير تعليقك على الآخرين وتأكد من اتباع إرشادات مجتمعنا", + "Copy Message": "نسخ الرسالة", + "Create Poll": "إنشاء تصويت", + "Delete": "حذف", + "Delete Message": "حذف الرسالة", + "Delete for me": "حذف لدي", + "Device camera is used to take photos or videos.": "تُستخدم كاميرا الجهاز لالتقاط الصور أو الفيديو.", + "Device gallery permissions is used to take photos or videos.": "تُستخدم أذونات معرض الجهاز لالتقاط الصور أو الفيديو.", + "Do you want to send a copy of this message to a moderator for further investigation?": "هل تريد إرسال نسخة من هذه الرسالة إلى مشرف لمزيد من المراجعة؟", + "Due since {{ dueSince }}": "مستحق منذ {{ dueSince }}", + "Edit Message": "تعديل الرسالة", + "Edited": "تم التعديل", + "Editing Message": "جارٍ تعديل الرسالة", + "Emoji matching": "مطابقة الإيموجي", + "Empty message...": "رسالة فارغة...", + "End Vote": "إنهاء التصويت", + "Error loading": "خطأ في التحميل", + "Error loading channel list...": "خطأ في تحميل قائمة القنوات...", + "Error loading messages for this channel...": "خطأ في تحميل رسائل هذه القناة...", + "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "خطأ في وضع علامة غير مقروء. لا يمكن وضع علامة غير مقروء للرسائل الأقدم من أحدث 100 رسالة في القناة.", + "Error while loading, please reload/refresh": "حدث خطأ أثناء التحميل، يرجى إعادة التحميل/التحديث", + "File is too large: {{ size }}, maximum upload size is {{ limit }}": "الملف كبير جدًا: {{ size }}، الحد الأقصى للرفع هو {{ limit }}", + "File type not supported": "نوع الملف غير مدعوم", + "Flag": "إبلاغ", + "Flag Message": "الإبلاغ عن الرسالة", + "Flag action failed either due to a network issue or the message is already flagged": "فشلت عملية الإبلاغ بسبب مشكلة في الشبكة أو لأن الرسالة مبلّغ عنها بالفعل.", + "Generating...": "جارٍ الإنشاء...", + "Hold to start recording.": "اضغط مطولاً لبدء التسجيل.", + "How about sending your first message to a friend?": "ما رأيك في إرسال أول رسالة إلى صديق؟", + "Instant Commands": "الأوامر السريعة", + "Let's start chatting!": "لنبدأ الدردشة!", + "Links are disabled": "الروابط معطلة", + "Loading channels...": "جارٍ تحميل القنوات...", + "Loading messages...": "جارٍ تحميل الرسائل...", + "Loading threads...": "جارٍ تحميل المواضيع...", + "Loading...": "جارٍ التحميل...", + "Location": "الموقع", + "Mark as Unread": "وضع كغير مقروء", + "Maximum number of files reached": "تم الوصول إلى الحد الأقصى لعدد الملفات", + "Message Reactions": "تفاعلات الرسالة", + "Message deleted": "تم حذف الرسالة", + "Message flagged": "تم الإبلاغ عن الرسالة", + "Multiple votes": "تصويتات متعددة", + "Select more than one option": "اختر أكثر من خيار واحد", + "Limit votes per person": "تحديد عدد الأصوات لكل شخص", + "Choose between 2–10 options": "اختر بين 2 و10 خيارات", + "Mute User": "كتم المستخدم", + "No chats here yet…": "لا توجد محادثات هنا بعد…", + "No threads here yet": "لا توجد مواضيع هنا بعد", + "Not supported": "غير مدعوم", + "Nothing yet...": "لا شيء بعد...", + "Ok": "حسناً", + "Only visible to you": "مرئي لك فقط", + "Open Settings": "فتح الإعدادات", + "Option": "خيار", + "Option {{count}}": "الخيار {{count}}", + "Option already exists": "الخيار موجود بالفعل", + "Options": "الخيارات", + "Photo": "صورة", + "Photos and Videos": "الصور ومقاطع الفيديو", + "Pin to Conversation": "تثبيت في المحادثة", + "Pinned by": "مثبّت بواسطة", + "Please allow Audio permissions in settings.": "يرجى السماح بأذونات الصوت في الإعدادات.", + "Please enable access to your photos and videos so you can share them.": "يرجى تفعيل الوصول إلى صورك ومقاطع الفيديو حتى تتمكن من مشاركتها.", + "Please select a channel first": "يرجى اختيار قناة أولاً", + "Poll Comments": "تعليقات التصويت", + "Poll Options": "خيارات التصويت", + "Poll Results": "نتائج التصويت", + "Questions": "الأسئلة", + "Reconnecting...": "جارٍ إعادة الاتصال...", + "Reply": "رد", + "Reply to Message": "الرد على الرسالة", + "Resend": "إعادة الإرسال", + "SEND": "إرسال", + "Search": "بحث", + "Select More Photos": "اختيار المزيد من الصور", + "Select one": "اختر واحداً", + "Select one or more": "اختر واحداً أو أكثر", + "Select up to {{count}}_few": "اختر حتى {{count}}", + "Select up to {{count}}_many": "اختر حتى {{count}}", + "Select up to {{count}}_one": "اختر حتى {{count}}", + "Select up to {{count}}_other": "اختر حتى {{count}}", + "Select up to {{count}}_two": "اختر حتى {{count}}", + "Select up to {{count}}_zero": "اختر حتى {{count}}", + "Send Anyway": "أرسل على أي حال", + "Send a message": "أرسل رسالة", + "Sending links is not allowed in this conversation": "إرسال الروابط غير مسموح في هذه المحادثة", + "Show All": "عرض الكل", + "Slide to Cancel": "اسحب للإلغاء", + "Slow mode ON": "الوضع البطيء قيد التشغيل", + "Slow mode, wait {{seconds}}s...": "وضع بطيء، انتظر {{seconds}}ث...", + "Suggest an option": "اقترح خياراً", + "The message has been reported to a moderator.": "تم الإبلاغ عن الرسالة إلى مشرف.", + "The source message was deleted": "تم حذف الرسالة الأصلية", + "Thinking...": "جارٍ التفكير...", + "This reply was deleted": "تم حذف هذا الرد", + "Thread Reply": "رد في الموضوع", + "Type a number from 2 to 10": "اكتب رقماً من 2 إلى 10", + "Unban User": "إلغاء منع المستخدم", + "Unblock User": "إلغاء حظر المستخدم", + "Unknown User": "مستخدم غير معروف", + "Unmute User": "إلغاء كتم المستخدم", + "Unpin from Conversation": "إلغاء التثبيت من المحادثة", + "Unread Messages": "رسائل غير مقروءة", + "Update your comment": "حدّث تعليقك", + "Video": "فيديو", + "View Results": "عرض النتائج", + "View {{count}} comments_few": "عرض {{count}} تعليقات", + "View {{count}} comments_many": "عرض {{count}} تعليقات", + "View {{count}} comments_one": "عرض {{count}} تعليق", + "View {{count}} comments_other": "عرض {{count}} تعليقات", + "View {{count}} comments_two": "عرض تعليقين", + "View {{count}} comments_zero": "عرض التعليقات", + "Voice message": "رسالة صوتية", + "You": "أنت", + "You can't send messages in this channel": "لا يمكنك إرسال رسائل في هذه القناة", + "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", + "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", + "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", + "replied to": "رد على", + "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[أمس]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[غدًا]\", \"nextWeek\":\"dddd [في] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", + "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | relativeCompactDateFormatter }}", + "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", + "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[أمس]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[غدًا]\", \"nextWeek\":\"dddd [في] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", + "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} و{{ nonSelfUserLength }} آخرون يكتبون", + "{{ index }} of {{ photoLength }}": "{{ index }} من {{ photoLength }}", + "{{ replyCount }} Replies": "{{ replyCount }} ردود", + "{{ user }} is typing": "{{ user }} يكتب", + "You voted: {{ option }}": "لقد صوّتَّ: {{ option }}", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} و{{ secondUser }} يكتبان", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} أشخاص يكتبون", + "Typing": "يكتب", + "No messages yet": "لا توجد رسائل بعد", + "Message failed to send": "فشل إرسال الرسالة", + "and {{ count }} others": "و{{ count }} آخرون", + "{{ user }} voted: {{ option }}": "{{ user }} صوّت: {{ option }}", + "{{count}} votes_few": "{{count}} أصوات", + "{{count}} votes_many": "{{count}} أصوات", + "{{count}} votes_one": "{{count}} صوت", + "{{count}} votes_other": "{{count}} أصوات", + "{{count}} votes_two": "صوتان", + "{{count}} votes_zero": "لا أصوات", + "🏙 Attachment...": "🏙 مرفق...", + "You have not granted access to the photo library.": "لم تمنح إذن الوصول إلى مكتبة الصور.", + "Change in Settings": "التغيير في الإعدادات", + "Create a poll and let everyone vote": "أنشئ تصويتاً ودع الجميع يصوّت", + "Open Camera": "فتح الكاميرا", + "Pick a document to share it with everyone": "اختر مستندًا لمشاركته مع الجميع", + "Pick document": "اختر مستنداً", + "Take a video and share": "التقط فيديو وشاركه", + "You have not granted access to your camera": "لم تمنح إذن الوصول إلى الكاميرا", + "{{count}} Reactions_few": "{{count}} تفاعلات", + "{{count}} Reactions_many": "{{count}} تفاعلات", + "{{count}} Reactions_one": "{{count}} تفاعل", + "{{count}} Reactions_other": "{{count}} تفاعلات", + "{{count}} Reactions_two": "تفاعلان", + "{{count}} Reactions_zero": "لا تفاعلات", + "Tap to remove": "اضغط للإزالة", + "Draft": "مسودة", + "Reminder set": "تم ضبط التذكير", + "Also sent in channel": "تم الإرسال أيضًا في القناة", + "Replied to a thread": "تم الرد في موضوع", + "View": "عرض", + "Reminder overdue": "انتهى وقت التذكير", + "Poll has ended": "انتهى التصويت", + "Reply to a message to start a thread": "قم بالرد على رسالة لبدء موضوع", + "Couldn't load new threads. Tap to retry": "تعذر تحميل المواضيع الجديدة. اضغط لإعادة المحاولة", + "{{count}} new threads": "{{count}} مواضيع جديدة", + "No conversations yet": "لا توجد محادثات بعد", + "Are you sure you want to delete this group? This can't be undone.": "هل أنت متأكد أنك تريد حذف هذه المجموعة؟ لا يمكن التراجع عن هذا.", + "Are you sure you want to delete this chat? This can't be undone.": "هل أنت متأكد أنك تريد حذف هذه الدردشة؟ لا يمكن التراجع عن هذا.", + "Delete chat": "حذف الدردشة", + "Delete group": "حذف المجموعة", + "Archive Chat": "أرشفة الدردشة", + "Archive Group": "أرشفة المجموعة", + "Delete Chat": "حذف الدردشة", + "Delete Group": "حذف المجموعة", + "Leave Chat": "مغادرة الدردشة", + "Leave Group": "مغادرة المجموعة", + "Mute Group": "كتم المجموعة", + "Offline": "غير متصل", + "Online": "متصل", + "Unarchive Chat": "إلغاء أرشفة الدردشة", + "Unarchive Group": "إلغاء أرشفة المجموعة", + "Unmute Group": "إلغاء كتم المجموعة", + "{{memberCount}} members, {{onlineCount}} online_few": "{{memberCount}} أعضاء، {{onlineCount}} متصل", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} أعضاء، {{onlineCount}} متصل", + "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} عضو، {{onlineCount}} متصل", + "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} أعضاء، {{onlineCount}} متصل", + "{{memberCount}} members, {{onlineCount}} online_two": "عضوان، {{onlineCount}} متصل", + "{{memberCount}} members, {{onlineCount}} online_zero": "لا أعضاء", + "{{count}} unread": "{{count}} غير مقروء", + "{{count}} new messages": "{{count}} رسائل جديدة", + "Unsupported Attachment": "مرفق غير مدعوم", + "+{{count}} More Options_few": "+{{count}} خيارات إضافية", + "+{{count}} More Options_many": "+{{count}} خيارات إضافية", + "+{{count}} More Options_one": "+{{count}} خيار إضافي", + "+{{count}} More Options_other": "+{{count}} خيارات إضافية", + "+{{count}} More Options_two": "+{{count}} خيارات إضافية", + "+{{count}} More Options_zero": "+{{count}} خيارات إضافية" +} diff --git a/package/src/utils/__tests__/Streami18n.test.js b/package/src/utils/__tests__/Streami18n.test.js index a3152bf09a..52531fcc7f 100644 --- a/package/src/utils/__tests__/Streami18n.test.js +++ b/package/src/utils/__tests__/Streami18n.test.js @@ -1,8 +1,10 @@ import { default as Dayjs } from 'dayjs'; +import 'dayjs/locale/ar'; import 'dayjs/locale/nl'; import localeData from 'dayjs/plugin/localeData'; import moment from 'moment-timezone'; +import arTranslations from '../../i18n/ar.json'; import frTranslations from '../../i18n/fr.json'; import nlTranslations from '../../i18n/nl.json'; import { Streami18n } from '../i18n/Streami18n'; @@ -43,6 +45,69 @@ describe('Streami18n instance - default', () => { }); }); +describe('Streami18n instance - Arabic language (ar)', () => { + describe('datetime translations enabled', () => { + const streami18nOptions = { language: 'ar' }; + const streami18n = new Streami18n(streami18nOptions); + it('should provide arabic translator', async () => { + const { t: _t } = await streami18n.getTranslators(); + for (const key in arTranslations) { + const value = arTranslations[key]; + const hasTemplateInKey = key.indexOf('{{') > -1 && key.indexOf('}}') > -1; + const hasTemplateInValue = + typeof value === 'string' && value.indexOf('{{') > -1 && value.indexOf('}}') > -1; + if ( + hasTemplateInKey || + hasTemplateInValue || + key.indexOf('duration/') > -1 || + key.indexOf('timestamp/') > -1 + ) { + continue; + } + expect(_t(key)).toBe(arTranslations[key]); + } + }); + it('should provide dayjs with `ar` locale', async () => { + const { tDateTimeParser } = await streami18n.getTranslators(); + expect(tDateTimeParser() instanceof Dayjs).toBe(true); + expect(tDateTimeParser().locale()).toBe('ar'); + }); + }); + + describe('datetime translations disabled', () => { + const streami18nOptions = { + disableDateTimeTranslations: true, + language: 'ar', + }; + const streami18n = new Streami18n(streami18nOptions); + + it('should provide arabic translator', async () => { + const { t: _t } = await streami18n.getTranslators(); + for (const key in arTranslations) { + const value = arTranslations[key]; + const hasTemplateInKey = key.indexOf('{{') > -1 && key.indexOf('}}') > -1; + const hasTemplateInValue = + typeof value === 'string' && value.indexOf('{{') > -1 && value.indexOf('}}') > -1; + if ( + hasTemplateInKey || + hasTemplateInValue || + key.indexOf('duration/') > -1 || + key.indexOf('timestamp/') > -1 + ) { + continue; + } + expect(_t(key)).toBe(arTranslations[key]); + } + }); + + it('should provide dayjs with default `en` locale when datetime translations disabled', async () => { + const { tDateTimeParser } = await streami18n.getTranslators(); + expect(tDateTimeParser() instanceof Dayjs).toBe(true); + expect(tDateTimeParser().locale()).toBe('en'); + }); + }); +}); + describe('Streami18n instance - with built-in language', () => { describe('datetime translations enabled', () => { const streami18nOptions = { language: 'nl' }; diff --git a/package/src/utils/i18n/Streami18n.ts b/package/src/utils/i18n/Streami18n.ts index dfbf883a32..b268841def 100644 --- a/package/src/utils/i18n/Streami18n.ts +++ b/package/src/utils/i18n/Streami18n.ts @@ -15,6 +15,7 @@ import { predefinedFormatters } from './predefinedFormatters'; import { CustomFormatters, PredefinedFormatters } from './types'; import type { TDateTimeParser } from '../../contexts/translationContext/types'; +import arTranslations from '../../i18n/ar.json'; import enTranslations from '../../i18n/en.json'; import esTranslations from '../../i18n/es.json'; import frTranslations from '../../i18n/fr.json'; @@ -28,6 +29,7 @@ import ptBRTranslations from '../../i18n/pt-br.json'; import ruTranslations from '../../i18n/ru.json'; import trTranslations from '../../i18n/tr.json'; +import 'dayjs/locale/ar'; import 'dayjs/locale/es'; import 'dayjs/locale/fr'; import 'dayjs/locale/he'; @@ -65,6 +67,9 @@ Dayjs.updateLocale('en', { }, }); +Dayjs.updateLocale('ar', { + calendar: calendarFormats.ar, +}); Dayjs.updateLocale('es', { calendar: calendarFormats.es, }); @@ -393,6 +398,7 @@ export class Streami18n { [key: string]: Partial; }; } = { + ar: { [defaultNS]: arTranslations }, en: { [defaultNS]: enTranslations }, es: { [defaultNS]: esTranslations }, fr: { [defaultNS]: frTranslations }, diff --git a/package/src/utils/i18n/calendarFormats.ts b/package/src/utils/i18n/calendarFormats.ts index e4ffaf5c43..baa99162cd 100644 --- a/package/src/utils/i18n/calendarFormats.ts +++ b/package/src/utils/i18n/calendarFormats.ts @@ -4,6 +4,14 @@ import { CalendarFormats } from './Streami18n'; * Calendar formats for different languages. */ export const calendarFormats: Record = { + ar: { + lastDay: '[أمس]', + lastWeek: 'dddd', + nextDay: '[غدًا]', + nextWeek: 'dddd [عند] LT', + sameDay: '[اليوم]', + sameElse: 'L', + }, en: { lastDay: '[Yesterday]', lastWeek: 'dddd',