From 9f07887523adcb8b07a31c562bc1514afefc40fd Mon Sep 17 00:00:00 2001 From: Rohit Ninawe Date: Tue, 4 Jun 2024 15:50:26 +0530 Subject: [PATCH] v4.2.0 --- README.md | 16 +- .../CometchatUiKitPackage.java | 1 + .../TimeZoneCodeManager.java | 41 + ios/TimeZoneCodeManager.h | 6 + ios/TimeZoneCodeManager.m | 22 + package.json | 5 +- .../AIConversationStarterDecorator.tsx | 3 + .../AIConversationSummaryDecorator.tsx | 3 + .../AISmartRepliesDecorator.tsx | 3 + .../CometChatConversations.tsx | 6 +- .../CometChatMessageList.tsx | 5 +- src/index.ts | 6 + src/shared/CometChatUiKit/CometChatUIKit.ts | 33 +- src/shared/assets/images/BackArrow.png | Bin 0 -> 391 bytes src/shared/assets/images/calendar.png | Bin 0 -> 239 bytes src/shared/assets/images/clock-alert.png | Bin 0 -> 1945 bytes src/shared/assets/images/clock.png | Bin 0 -> 999 bytes src/shared/assets/images/earth.png | Bin 0 -> 1724 bytes src/shared/assets/images/empty-slot.png | Bin 0 -> 663 bytes src/shared/assets/images/forwardArrow.png | Bin 0 -> 391 bytes src/shared/assets/images/index.ts | 12 + src/shared/constants/UIKitConstants.ts | 2 + .../CometChatUIEventHandler.ts | 3 + .../CometChatUIEventHandler/Listener.ts | 4 + src/shared/events/ListenerInitializer.ts | 3 + src/shared/events/messages.ts | 1 + src/shared/framework/DataSource.ts | 6 + src/shared/framework/DataSourceDecorator.tsx | 17 + src/shared/framework/MessageDataSource.tsx | 43 +- src/shared/index.ts | 8 +- .../Calendars/BaseCalendar.tsx | 226 ++ .../Calendars/DateSelectionCalendar.tsx | 88 + .../Calendars/MultiDateSelectionCalendar.tsx | 106 + .../CometChatCalendar/Calendars/Providers.tsx | 27 + .../libs/CometChatCalendar/Calendars/index.ts | 3 + .../Components/Arrow.test.tsx | 112 + .../CometChatCalendar/Components/Arrow.tsx | 46 + .../CometChatCalendar/Components/Day.test.tsx | 396 +++ .../libs/CometChatCalendar/Components/Day.tsx | 93 + .../Components/Days.test.tsx | 216 ++ .../CometChatCalendar/Components/Days.tsx | 109 + .../Components/Month.test.tsx | 160 ++ .../CometChatCalendar/Components/Month.tsx | 46 + .../Components/Months.test.tsx | 262 ++ .../CometChatCalendar/Components/Months.tsx | 78 + .../Components/Title.test.tsx | 111 + .../CometChatCalendar/Components/Title.tsx | 45 + .../Components/Weekdays.test.tsx | 69 + .../CometChatCalendar/Components/Weekdays.tsx | 29 + .../CometChatCalendar/Components/index.ts | 7 + .../libs/CometChatCalendar/Constants/View.ts | 4 + .../libs/CometChatCalendar/Constants/index.ts | 1 + .../Contexts/LocaleContext.tsx | 7 + .../Contexts/ThemeContext.tsx | 7 + .../libs/CometChatCalendar/Contexts/index.ts | 2 + .../Entities/ArrowDirections.ts | 1 + .../CometChatCalendar/Entities/Calendar.ts | 26 + .../CometChatCalendar/Entities/Components.ts | 13 + .../Entities/DateProperties.ts | 6 + .../libs/CometChatCalendar/Entities/Locale.ts | 12 + .../libs/CometChatCalendar/Entities/Theme.ts | 70 + .../libs/CometChatCalendar/Entities/index.ts | 6 + .../libs/CometChatCalendar/Hooks/index.ts | 1 + .../Hooks/useSurroundingTimeUnits.test.ts | 44 + .../Hooks/useSurroundingTimeUnits.ts | 60 + .../CometChatCalendar/Icons/BackArrow.png | Bin 0 -> 391 bytes .../Icons/chevron-left-16.png | Bin 0 -> 217 bytes .../libs/CometChatCalendar/Icons/index.ts | 7 + .../libs/CometChatCalendar/Locales/index.ts | 3 + .../libs/CometChatCalendar/Themes/Colors.ts | 31 + .../CometChatCalendar/Themes/DarkTheme.ts | 79 + .../CometChatCalendar/Themes/DefaultTheme.ts | 164 ++ .../libs/CometChatCalendar/Themes/index.ts | 3 + .../Utils/addOpacity.test.ts | 49 + .../CometChatCalendar/Utils/addOpacity.ts | 15 + .../Utils/checkChangedProps.ts | 10 + .../CometChatCalendar/Utils/clamp.test.ts | 39 + .../libs/CometChatCalendar/Utils/clamp.ts | 5 + .../CometChatCalendar/Utils/dateRange.test.ts | 20 + .../libs/CometChatCalendar/Utils/dateRange.ts | 22 + .../Utils/getExtraDays.test.ts | 48 + .../CometChatCalendar/Utils/getExtraDays.ts | 24 + .../Utils/getSurroundingTimeUnits.test.ts | 60 + .../Utils/getSurroundingTimeUnits.ts | 19 + .../libs/CometChatCalendar/Utils/index.ts | 6 + src/shared/libs/CometChatCalendar/index.ts | 5 + .../DateTimePickerModal.android.js | 81 + .../DateTimePickerModal.ios.js | 363 +++ .../datePickerModal/DateTimePickerModal.js | 9 + src/shared/libs/datePickerModal/Modal.js | 180 ++ src/shared/libs/datePickerModal/index.d.ts | 310 +++ src/shared/libs/datePickerModal/index.js | 5 + src/shared/libs/datePickerModal/utils.js | 25 + src/shared/libs/luxon/src/datetime.js | 2422 +++++++++++++++++ src/shared/libs/luxon/src/duration.js | 990 +++++++ src/shared/libs/luxon/src/errors.js | 61 + src/shared/libs/luxon/src/impl/conversions.js | 206 ++ src/shared/libs/luxon/src/impl/diff.js | 95 + src/shared/libs/luxon/src/impl/digits.js | 75 + src/shared/libs/luxon/src/impl/english.js | 233 ++ src/shared/libs/luxon/src/impl/formats.js | 176 ++ src/shared/libs/luxon/src/impl/formatter.js | 409 +++ src/shared/libs/luxon/src/impl/invalid.js | 14 + src/shared/libs/luxon/src/impl/locale.js | 542 ++++ src/shared/libs/luxon/src/impl/regexParser.js | 335 +++ src/shared/libs/luxon/src/impl/tokenParser.js | 473 ++++ src/shared/libs/luxon/src/impl/util.js | 309 +++ src/shared/libs/luxon/src/impl/zoneUtil.js | 34 + src/shared/libs/luxon/src/info.js | 205 ++ src/shared/libs/luxon/src/interval.js | 657 +++++ src/shared/libs/luxon/src/luxon.js | 26 + src/shared/libs/luxon/src/package.json | 4 + src/shared/libs/luxon/src/settings.js | 175 ++ src/shared/libs/luxon/src/zone.js | 91 + src/shared/libs/luxon/src/zones/IANAZone.js | 189 ++ .../libs/luxon/src/zones/fixedOffsetZone.js | 102 + .../libs/luxon/src/zones/invalidZone.js | 53 + src/shared/libs/luxon/src/zones/systemZone.js | 61 + .../InteractiveElements/DateTimeElement.ts | 103 + .../InteractiveEntities/ElementEntity.ts | 5 +- .../InteractiveMessage/SchedulerMessage.ts | 149 + .../InteractiveMessage/index.ts | 1 + src/shared/modals/InteractiveData/index.ts | 3 +- src/shared/modals/index.ts | 5 +- .../resources/ar/translation.json | 592 ++-- .../resources/de/translation.json | 592 ++-- .../resources/en/translation.json | 48 +- .../resources/es/translation.json | 592 ++-- .../resources/fr/translation.json | 592 ++-- .../resources/hi/translation.json | 592 ++-- .../resources/lt/translation.json | 592 ++-- .../resources/ms/translation.json | 592 ++-- .../resources/pt/translation.json | 592 ++-- .../resources/ru/translation.json | 592 ++-- .../resources/sv/translation.json | 592 ++-- .../resources/zh-tw/translation.json | 592 ++-- .../resources/zh/translation.json | 593 ++-- .../resources/CometChatTheme/Palette.ts | 30 + .../resources/CometChatTheme/Typography.ts | 109 + src/shared/utils/InteractiveMessageUtils.ts | 5 +- src/shared/utils/SchedulerUtils.ts | 196 ++ src/shared/utils/conversationUtils.ts | 9 +- src/shared/utils/icsToJson.js | 113 + .../CometChatCheckBox/CometChatCheckBox.tsx | 6 +- .../CometChatDateTimePicker.tsx | 189 ++ .../DateTimePickerStyle.ts | 19 + .../views/CometChatDateTimePicker/index.ts | 5 + .../CometChatDropDown/CometChatDropDown.tsx | 6 +- .../CometChatFormBubble.tsx | 127 +- .../CometChatFormBubble/FormBubbleStyle.tsx | 5 + .../views/CometChatLabel/CometChatLabel.tsx | 2 +- .../CometChatRadioButton.tsx | 6 +- .../CometChatSchedulerBubble.tsx | 1500 ++++++++++ .../views/CometChatSchedulerBubble/index.ts | 5 + .../views/CometChatSchedulerBubble/styles.ts | 92 + .../CometChatSingleSelect.tsx | 8 +- .../CometChatTextInput/CometChatTextInput.tsx | 8 +- .../CometChatTimeSlotSelector.tsx | 113 + .../views/CometChatTimeSlotSelector/index.ts | 5 + .../views/CometChatTimeSlotSelector/styles.ts | 30 + src/shared/views/index.ts | 5 +- 161 files changed, 18693 insertions(+), 3576 deletions(-) create mode 100644 android/src/main/java/com/reactnativecometchatuikit/TimeZoneCodeManager.java create mode 100644 ios/TimeZoneCodeManager.h create mode 100644 ios/TimeZoneCodeManager.m create mode 100644 src/shared/assets/images/BackArrow.png create mode 100644 src/shared/assets/images/calendar.png create mode 100644 src/shared/assets/images/clock-alert.png create mode 100644 src/shared/assets/images/clock.png create mode 100644 src/shared/assets/images/earth.png create mode 100644 src/shared/assets/images/empty-slot.png create mode 100644 src/shared/assets/images/forwardArrow.png create mode 100644 src/shared/libs/CometChatCalendar/Calendars/BaseCalendar.tsx create mode 100644 src/shared/libs/CometChatCalendar/Calendars/DateSelectionCalendar.tsx create mode 100644 src/shared/libs/CometChatCalendar/Calendars/MultiDateSelectionCalendar.tsx create mode 100644 src/shared/libs/CometChatCalendar/Calendars/Providers.tsx create mode 100644 src/shared/libs/CometChatCalendar/Calendars/index.ts create mode 100644 src/shared/libs/CometChatCalendar/Components/Arrow.test.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Arrow.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Day.test.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Day.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Days.test.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Days.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Month.test.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Month.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Months.test.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Months.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Title.test.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Title.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Weekdays.test.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/Weekdays.tsx create mode 100644 src/shared/libs/CometChatCalendar/Components/index.ts create mode 100644 src/shared/libs/CometChatCalendar/Constants/View.ts create mode 100644 src/shared/libs/CometChatCalendar/Constants/index.ts create mode 100644 src/shared/libs/CometChatCalendar/Contexts/LocaleContext.tsx create mode 100644 src/shared/libs/CometChatCalendar/Contexts/ThemeContext.tsx create mode 100644 src/shared/libs/CometChatCalendar/Contexts/index.ts create mode 100644 src/shared/libs/CometChatCalendar/Entities/ArrowDirections.ts create mode 100644 src/shared/libs/CometChatCalendar/Entities/Calendar.ts create mode 100644 src/shared/libs/CometChatCalendar/Entities/Components.ts create mode 100644 src/shared/libs/CometChatCalendar/Entities/DateProperties.ts create mode 100644 src/shared/libs/CometChatCalendar/Entities/Locale.ts create mode 100644 src/shared/libs/CometChatCalendar/Entities/Theme.ts create mode 100644 src/shared/libs/CometChatCalendar/Entities/index.ts create mode 100644 src/shared/libs/CometChatCalendar/Hooks/index.ts create mode 100644 src/shared/libs/CometChatCalendar/Hooks/useSurroundingTimeUnits.test.ts create mode 100644 src/shared/libs/CometChatCalendar/Hooks/useSurroundingTimeUnits.ts create mode 100644 src/shared/libs/CometChatCalendar/Icons/BackArrow.png create mode 100644 src/shared/libs/CometChatCalendar/Icons/chevron-left-16.png create mode 100644 src/shared/libs/CometChatCalendar/Icons/index.ts create mode 100644 src/shared/libs/CometChatCalendar/Locales/index.ts create mode 100644 src/shared/libs/CometChatCalendar/Themes/Colors.ts create mode 100644 src/shared/libs/CometChatCalendar/Themes/DarkTheme.ts create mode 100644 src/shared/libs/CometChatCalendar/Themes/DefaultTheme.ts create mode 100644 src/shared/libs/CometChatCalendar/Themes/index.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/addOpacity.test.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/addOpacity.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/checkChangedProps.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/clamp.test.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/clamp.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/dateRange.test.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/dateRange.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/getExtraDays.test.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/getExtraDays.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/getSurroundingTimeUnits.test.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/getSurroundingTimeUnits.ts create mode 100644 src/shared/libs/CometChatCalendar/Utils/index.ts create mode 100644 src/shared/libs/CometChatCalendar/index.ts create mode 100644 src/shared/libs/datePickerModal/DateTimePickerModal.android.js create mode 100644 src/shared/libs/datePickerModal/DateTimePickerModal.ios.js create mode 100644 src/shared/libs/datePickerModal/DateTimePickerModal.js create mode 100644 src/shared/libs/datePickerModal/Modal.js create mode 100644 src/shared/libs/datePickerModal/index.d.ts create mode 100644 src/shared/libs/datePickerModal/index.js create mode 100644 src/shared/libs/datePickerModal/utils.js create mode 100644 src/shared/libs/luxon/src/datetime.js create mode 100644 src/shared/libs/luxon/src/duration.js create mode 100644 src/shared/libs/luxon/src/errors.js create mode 100644 src/shared/libs/luxon/src/impl/conversions.js create mode 100644 src/shared/libs/luxon/src/impl/diff.js create mode 100644 src/shared/libs/luxon/src/impl/digits.js create mode 100644 src/shared/libs/luxon/src/impl/english.js create mode 100644 src/shared/libs/luxon/src/impl/formats.js create mode 100644 src/shared/libs/luxon/src/impl/formatter.js create mode 100644 src/shared/libs/luxon/src/impl/invalid.js create mode 100644 src/shared/libs/luxon/src/impl/locale.js create mode 100644 src/shared/libs/luxon/src/impl/regexParser.js create mode 100644 src/shared/libs/luxon/src/impl/tokenParser.js create mode 100644 src/shared/libs/luxon/src/impl/util.js create mode 100644 src/shared/libs/luxon/src/impl/zoneUtil.js create mode 100644 src/shared/libs/luxon/src/info.js create mode 100644 src/shared/libs/luxon/src/interval.js create mode 100644 src/shared/libs/luxon/src/luxon.js create mode 100644 src/shared/libs/luxon/src/package.json create mode 100644 src/shared/libs/luxon/src/settings.js create mode 100644 src/shared/libs/luxon/src/zone.js create mode 100644 src/shared/libs/luxon/src/zones/IANAZone.js create mode 100644 src/shared/libs/luxon/src/zones/fixedOffsetZone.js create mode 100644 src/shared/libs/luxon/src/zones/invalidZone.js create mode 100644 src/shared/libs/luxon/src/zones/systemZone.js create mode 100644 src/shared/modals/InteractiveData/InteractiveElements/DateTimeElement.ts create mode 100644 src/shared/modals/InteractiveData/InteractiveMessage/SchedulerMessage.ts create mode 100644 src/shared/utils/SchedulerUtils.ts create mode 100644 src/shared/utils/icsToJson.js create mode 100644 src/shared/views/CometChatDateTimePicker/CometChatDateTimePicker.tsx create mode 100644 src/shared/views/CometChatDateTimePicker/DateTimePickerStyle.ts create mode 100644 src/shared/views/CometChatDateTimePicker/index.ts create mode 100644 src/shared/views/CometChatSchedulerBubble/CometChatSchedulerBubble.tsx create mode 100644 src/shared/views/CometChatSchedulerBubble/index.ts create mode 100644 src/shared/views/CometChatSchedulerBubble/styles.ts create mode 100644 src/shared/views/CometChatTimeSlotSelector/CometChatTimeSlotSelector.tsx create mode 100644 src/shared/views/CometChatTimeSlotSelector/index.ts create mode 100644 src/shared/views/CometChatTimeSlotSelector/styles.ts diff --git a/README.md b/README.md index 3806096..8ab807a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## Table of contents - [About the project](#about-the-project) - - [Built With](#built-with) + - [Prerequisites](#prerequisites) - [Getting Started](#getting-started) - [Dependencies](#dependencies) - [Versioning](#versioning) @@ -29,13 +29,12 @@ ## About the project CometChat React Native UIKit provides pre-built user interface kit that developers can use to quickly integrate a reliable & fully featured chat experience into an existing or a new mobile app.
-### Built With -- react: 17.0.2 -- react native: 0.68.5 -- Android Studio 2021.3 dolphin -- Xcode 14.1 -- Android 9, API 28 and above -- iOS 12.0 +### Prerequisites +- compileSdkVersion >= 33 +- @react-native-community/datetimepicker +- @react-native-community/async-storage +- @cometchat/chat-sdk-react-native +- @react-native-community/clipboard ## Getting Started To set up React Native Chat UIKit and utilize CometChat for your chat functionality, you'll need to follow these steps: @@ -47,6 +46,7 @@ To set up React Native Chat UIKit and utilize CometChat for your chat functional ### Dependencies To utilize the React Native Chat UIKit in your React native project, you need to include the necessary dependencies in your file.
+ @react-native-community/datetimepicker
@react-native-community/async-storage
@cometchat/chat-sdk-react-native
@react-native-community/clipboard
diff --git a/android/src/main/java/com/reactnativecometchatuikit/CometchatUiKitPackage.java b/android/src/main/java/com/reactnativecometchatuikit/CometchatUiKitPackage.java index 84ec4ea..2b37f60 100644 --- a/android/src/main/java/com/reactnativecometchatuikit/CometchatUiKitPackage.java +++ b/android/src/main/java/com/reactnativecometchatuikit/CometchatUiKitPackage.java @@ -23,6 +23,7 @@ public List createNativeModules(@NonNull ReactApplicationContext r modules.add(new VideoManager(reactContext)); modules.add(new ImageManager(reactContext)); modules.add(new WebViewManager(reactContext)); + modules.add(new TimeZoneCodeManager(reactContext)); return modules; } diff --git a/android/src/main/java/com/reactnativecometchatuikit/TimeZoneCodeManager.java b/android/src/main/java/com/reactnativecometchatuikit/TimeZoneCodeManager.java new file mode 100644 index 0000000..bb703ff --- /dev/null +++ b/android/src/main/java/com/reactnativecometchatuikit/TimeZoneCodeManager.java @@ -0,0 +1,41 @@ +package com.reactnativecometchatuikit; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +import java.util.TimeZone; + +public class TimeZoneCodeManager extends ReactContextBaseJavaModule { + public static final String TAG = "TimeZoneCodeManager"; + + public TimeZoneCodeManager(ReactApplicationContext context) { + super(context); + } + + @ReactMethod + public void getCurrentTimeZone(Callback callback) { + try { + // Get the current time zone ID + String timeZoneID = TimeZone.getDefault().getID(); + // Invoke the callback with the time zone ID + callback.invoke(timeZoneID); + } catch (Exception e) { + // Log the exception + Log.e(TAG, "Error getting the time zone", e); + // If there's an error, you may choose to invoke the callback with null or an error message + callback.invoke((Object) null); + } + } + + @NonNull + @Override + public String getName() { + return TAG; + } +} \ No newline at end of file diff --git a/ios/TimeZoneCodeManager.h b/ios/TimeZoneCodeManager.h new file mode 100644 index 0000000..a5baf1f --- /dev/null +++ b/ios/TimeZoneCodeManager.h @@ -0,0 +1,6 @@ +#import +#import + +@interface TimeZoneCodeManager : NSObject + +@end diff --git a/ios/TimeZoneCodeManager.m b/ios/TimeZoneCodeManager.m new file mode 100644 index 0000000..32f4b4d --- /dev/null +++ b/ios/TimeZoneCodeManager.m @@ -0,0 +1,22 @@ +#import "TimeZoneCodeManager.h" +#import + +@implementation TimeZoneCodeManager { + +} + +// This macro makes the module available to JavaScript +RCT_EXPORT_MODULE(TimeZoneCodeManager); + +// Export your method to JavaScript using RCT_EXPORT_METHOD +RCT_EXPORT_METHOD(getCurrentTimeZone:(RCTResponseSenderBlock)callback) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Get the time zone string + NSString *timeZone = [[NSTimeZone localTimeZone] name]; + + // Call the callback block with the time zone string + callback(@[timeZone]); + }); +} + +@end diff --git a/package.json b/package.json index e51367d..a1afae0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cometchat/chat-uikit-react-native", - "version": "4.1.4", + "version": "4.2.0", "description": "CometChat React Native UI Kit is a collection of custom UI Components designed to build text , chat and calling features in your application. The UI Kit is developed to keep developers in mind and aims to reduce development efforts significantly", "main": "src/index", "module": "src/index", @@ -76,7 +76,8 @@ "peerDependencies": { "react": "*", "react-native": "*", - "@cometchat/chat-sdk-react-native": "*" + "@cometchat/chat-sdk-react-native": "*", + "@react-native-community/datetimepicker":"*" }, "jest": { "preset": "react-native", diff --git a/src/AI/AIConversationStarter/AIConversationStarterDecorator.tsx b/src/AI/AIConversationStarter/AIConversationStarterDecorator.tsx index f8cf572..7745b7c 100755 --- a/src/AI/AIConversationStarter/AIConversationStarterDecorator.tsx +++ b/src/AI/AIConversationStarter/AIConversationStarterDecorator.tsx @@ -197,6 +197,9 @@ export class AIConversationStarterDecorator extends DataSourceDecorator { onCardMessageReceived: (cardMessage) => { this.closeIfMessageReceived(cardMessage) }, + onSchedulerMessageReceived: (schedulerMessage) => { + this.closeIfMessageReceived(schedulerMessage) + }, onCustomInteractiveMessageReceived: (customInteractiveMessage) => { this.closeIfMessageReceived(customInteractiveMessage) } diff --git a/src/AI/AIConversationSummary/AIConversationSummaryDecorator.tsx b/src/AI/AIConversationSummary/AIConversationSummaryDecorator.tsx index 743a899..5b479a4 100644 --- a/src/AI/AIConversationSummary/AIConversationSummaryDecorator.tsx +++ b/src/AI/AIConversationSummary/AIConversationSummaryDecorator.tsx @@ -144,6 +144,9 @@ export class AIConversationSummaryDecorator extends DataSourceDecorator { onCardMessageReceived: (cardMessage) => { this.closeIfMessageReceived(cardMessage) }, + onSchedulerMessageReceived: (schedulerMessage) => { + this.closeIfMessageReceived(schedulerMessage) + }, onCustomInteractiveMessageReceived: (customInteractiveMessage) => { this.closeIfMessageReceived(customInteractiveMessage) } diff --git a/src/AI/AISmartReplies/AISmartRepliesDecorator.tsx b/src/AI/AISmartReplies/AISmartRepliesDecorator.tsx index 9017dc6..203831c 100755 --- a/src/AI/AISmartReplies/AISmartRepliesDecorator.tsx +++ b/src/AI/AISmartReplies/AISmartRepliesDecorator.tsx @@ -267,6 +267,9 @@ export class AISmartRepliesExtensionDecorator extends DataSourceDecorator { onCardMessageReceived: (cardMessage) => { this.closeIfMessageReceived(cardMessage) }, + onSchedulerMessageReceived: (schedulerMessage) => { + this.closeIfMessageReceived(schedulerMessage) + }, onCustomInteractiveMessageReceived: (customInteractiveMessage) => { this.closeIfMessageReceived(customInteractiveMessage) } diff --git a/src/CometChatConversations/CometChatConversations.tsx b/src/CometChatConversations/CometChatConversations.tsx index fd3951b..cb14924 100644 --- a/src/CometChatConversations/CometChatConversations.tsx +++ b/src/CometChatConversations/CometChatConversations.tsx @@ -373,7 +373,7 @@ export const CometChatConversations = (props: ConversationInterface) => { let isTyping = args[1]; let newConversation = conversation if (isTyping && newConversation?.['lastMessage']?.["typing"]) { - newConversation['lastMessage']["typing"] = args[0].receiverType === 'group' ? + newConversation['lastMessage']["typing"] = args[0]?.receiverType === 'group' ? `${args[0].sender.name} : ${localize("IS_TYPING")}` : localize("IS_TYPING"); } else { @@ -837,6 +837,10 @@ export const CometChatConversations = (props: ConversationInterface) => { messageEventHandler(cardMessage); !disableSoundForMessages && CometChatSoundManager.play("incomingMessage"); }, + onSchedulerMessageReceived: (schedulerMessage) => { + messageEventHandler(schedulerMessage); + !disableSoundForMessages && CometChatSoundManager.play("incomingMessage"); + }, onCustomInteractiveMessageReceived: (customInteractiveMessage) => { messageEventHandler(customInteractiveMessage); !disableSoundForMessages && CometChatSoundManager.play("incomingMessage"); diff --git a/src/CometChatMessageList/CometChatMessageList.tsx b/src/CometChatMessageList/CometChatMessageList.tsx index 60ff97a..6664140 100644 --- a/src/CometChatMessageList/CometChatMessageList.tsx +++ b/src/CometChatMessageList/CometChatMessageList.tsx @@ -743,6 +743,9 @@ export const CometChatMessageList = forwardRef< onCardMessageReceived: (cardMessage) => { newMessage(cardMessage); }, + onSchedulerMessageReceived: (schedulerMessage) => { + newMessage(schedulerMessage); + }, onCustomInteractiveMessageReceived: (customInteractiveMessage) => { newMessage(customInteractiveMessage); }, @@ -1221,7 +1224,7 @@ export const CometChatMessageList = forwardRef< let bubbleAlignment: MessageBubbleAlignmentType = getAlignment(message); - return showOptions ? openOptionsForMessage(message, hasTemplate) : undefined} > + return showOptions ? openOptionsForMessage(message, hasTemplate) : undefined} > !isThreaded && getLeadingView(message)} diff --git a/src/index.ts b/src/index.ts index 8bff960..ae7ed58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -121,6 +121,8 @@ import { CometChatFormBubbleInterface, CometChatCardBubble, CometChatCardBubbleInterface, + CometChatSchedulerBubble, + CometChatSchedulerBubbleInterface, APIAction, ActionEntity, BaseInputElement, @@ -133,6 +135,7 @@ import { DropdownElement, ElementEntity, FormMessage, + SchedulerMessage, LabelElement, OptionElement, RadioButtonElement, @@ -614,6 +617,8 @@ export { CometChatFormBubbleInterface, CometChatCardBubble, CometChatCardBubbleInterface, + CometChatSchedulerBubble, + CometChatSchedulerBubbleInterface, APIAction, ActionEntity, BaseInputElement, @@ -626,6 +631,7 @@ export { DropdownElement, ElementEntity, FormMessage, + SchedulerMessage, LabelElement, OptionElement, RadioButtonElement, diff --git a/src/shared/CometChatUiKit/CometChatUIKit.ts b/src/shared/CometChatUiKit/CometChatUIKit.ts index 34d8940..b250db2 100644 --- a/src/shared/CometChatUiKit/CometChatUIKit.ts +++ b/src/shared/CometChatUiKit/CometChatUIKit.ts @@ -26,6 +26,7 @@ import { AIExtensionDataSource } from "../../AI/AIExtensionDataSource"; import { AISmartRepliesExtension } from "../../AI/AISmartReplies/AISmartReplies"; import { AIConversationSummaryExtension } from "../../AI/AIConversationSummary/AIConversationSummaryExtension"; import { AIAssistBotExtension } from "../../AI/AIAssistBot/AIAssistBotExtension"; +import { SchedulerMessage } from "../modals/InteractiveData/InteractiveMessage"; export class CometChatUIKit { static uiKitSettings: UIKitSettings; @@ -36,7 +37,6 @@ export class CometChatUIKit { CometChatUIKit.uiKitSettings = { ...uiKitSettings }; - console.log(uiKitSettings?.overrideAdminHost, uiKitSettings.overrideClientHost) var appSetting = new CometChat.AppSettingsBuilder() .subscribePresenceForAllUsers() .autoEstablishSocketConnection(uiKitSettings.autoEstablishSocketConnection) @@ -245,6 +245,37 @@ export class CometChatUIKit { }); } + + /** + * Sends a Scheduler message and emits events based on the message status. + * @param message - The Scheduler message to be sent. + * @param disableLocalEvents - A boolean indicating whether to disable local events or not. Default value is false. + */ + static sendSchedulerMessage(message: SchedulerMessage, disableLocalEvents: boolean = false): Promise { + return new Promise((resolve, reject) => { + if (!disableLocalEvents) { + CometChatUIKitHelper.onMessageSent(message, messageStatus.inprogress); + } + + CometChat.sendInteractiveMessage(message) + .then((message: CometChat.BaseMessage) => { + console.log("message sent successfully", message.getSentAt()) + if (!disableLocalEvents) { + CometChatUIKitHelper.onMessageSent(message, messageStatus.success); + } + resolve(message); + }) + .catch((error: CometChat.CometChatException) => { + console.log("error while sending message", { error }) + message.setMetadata({ error }); + // if (!disableLocalEvents) { + // CometChatUIKitHelper.onMessageSent(message, messageStatus.error); + // } + reject(error); + }); + }); + } + /** * Sends a FormMessage message and emits events based on the message status. * @param message - The FormMessage message to be sent. diff --git a/src/shared/assets/images/BackArrow.png b/src/shared/assets/images/BackArrow.png new file mode 100644 index 0000000000000000000000000000000000000000..e09440a84275dec623656c2c451f37f4cf348030 GIT binary patch literal 391 zcmeAS@N?(olHy`uVBq!ia0vp^Q6S901|%(3I5Gh#&H|6fVg?3oVGw3ym^DX&fq~J| z)5S5QV$R!J8@-qVMO-fmcUyD)+b*V*nfr&M`+4flCiTb&Z)Ef^k>h%E7RB9{o?xMXJzEC zhU|_SkD}dQ6t4?Zmj97aU!}r--=)r{sP2`xz(n^ADHE$T$~T?Roub-($0?F99P*}S7A@9C+lhR2>9FPQU5KI+Z1Q=5+Ft<;GomT_FLT0#HipN%(ePh^iO zezRR=_EP?xZ8v_aq-@Q literal 0 HcmV?d00001 diff --git a/src/shared/assets/images/calendar.png b/src/shared/assets/images/calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..81459596a1eb212267f51cf22b8ea60430df07d6 GIT binary patch literal 239 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND7etm z#WAE}&f95;Tn!35Zu{e9SG@}_JO4(iJE4&y^J!XN)du-P&*Tng95`=zvq;L8mvXRK7yB{M3E7W;?y*nozVwe?e<4 zFUDrYxN(p|Y^<)QTzKAa^wg+5M3 iVtMmoPS@`{$+qg4bc)))J$r$UW$<+Mb6Mw<&;$Tl_*stt literal 0 HcmV?d00001 diff --git a/src/shared/assets/images/clock-alert.png b/src/shared/assets/images/clock-alert.png new file mode 100644 index 0000000000000000000000000000000000000000..d57788922302eb6a473897660ee3d598a8b76f1e GIT binary patch literal 1945 zcmV;K2WI$*P)*nf4X>Qt*xoIT5)W9~`pd@;*re!Y* zDl_P5Vpc|>1(jhzRJJ{|GJ4R1p0-9%85Sf#(aTzwg0xaA+O*tVGbLT!i6duW+4lR+ znYnlFz27(UgA0q!-20z%X72Oc`vEyQIXO8wIXO8wIXPi50ze~xxd4^|SOs7$fS&-g z1K7o>o&RkuU$cboY2-X|lvD$l4xkA@3xI9_kzt}dzU4g60WdiC#{a1Y@F;-4EErr$ zod8}0a3+9?EIVTmfLQ>3bYS#CX+41JSWBj=nRx(y^Jw_KiL~6r+R7TLgMSXd;8B4@ z4f!;bA$0&&1YqDunVJENLYZ(bfUN--HA>J}UxX5-io1TMUCGS=mIGJ_;1U2Q05}4` zFaQHN(cfbLoCx4j0C)3Syv49=(!Q9>C@IJwpBOy&m)uZ~H|#3Xz2gAf2H+b5J*xp6 zgc4ySfYt;L`8U69$EB!^@_r1!KMC};@UsQYSO6PhK3F?f88r@d4(BRudrYkx_!)y_ zWK83C^N92i&zhG3*c(IlFWjpOuFhW@;~{ADdkVtgjF3aSujg ze467&2|0k6scf8S3jHv6V1@DxvGwM=b^vj$@(ZVS%_(Oq>&P_f{K%mLi2Ie#It7lX zR!%9W^;SfV9Z(5irJ~$*u9iFJDf{_D%ftaaUDQ7mW#5358KfAnbf-MXiaa=gxKKGt zrg2ef%uXO=KyfEDwTgtnu8Q+0lnQp98ToX0Y`EilGGNY5^Zs+;^Apl6J_iG z;&FwBRI1=-6!ZBt8F0X%im%}5G`gPKghFXC1sQMv@v5Mq!*-W+y5hmpT*(T}0VgYT zOmjKoNo%;skOROmM8$MG1dFr?h0)+FYYtc}=vZT`pb?4*DT-dmngb>aI`;Ai+L$Sd zxSfg&I$$7xoq`sMz&2*CPzVK0A%hMeJ{7dgfd!Tbg;01$Rvqw&poLy+>oDV5`x{t^eggf}pQT-1wC-_cpikJG-Yi!qbn2pU-YE;wK>S64_-9d$aK)m&XbzeIocP+rcMt@sKyCD^35 zz@hOkD571W(0!Drvo7g+g%e&6^kCE~-bQ5G0qY#DkMBE(Pt!ozo~pLN&4Ol$V)s(s z&Uj7XfNl?wbH%7poZu)??B1;|PyQONJe829aAzP4niP5^r28L+L(W$oX3=GPb}#{B z6){dW9I(svxpLaWEV}txTDJ2+vr?yoCJ&pdgd#L8`=L@;#JkO1pSLzShCT^xK2}!4 zw9u^J*D=JkIXoo00>Bp@9I(dq*Iug}AM~!0o_BB<>WmngY&szF(fJq?(+LruVtY?Y zQP$NwD);4pO+J2;pq4+sC}!FL;0enczHd*ULn3Z|q`^5u6kD63B8jHG?*K5tpdPJN z9IxQQ0oz!MbEtQ;DCV)iQGwCrJ6ui8_UmOd50R+ixd2QSi54T zu`=cEkOE_b#&Vc2bvF!n9i*vDT7cFgrvF^B%<`X;Z_{;*o^r zSbHh$D5oEa0|!S%jvQd)3yAplbIO8=7L7s!CKq2o#BE7hJ4~Ij+>$}GJKMV)kLrV` z)1pDB(KkK1A?`d^$ggw^-cs%Q400000NkvXXu0mjfE4zJJ literal 0 HcmV?d00001 diff --git a/src/shared/assets/images/clock.png b/src/shared/assets/images/clock.png new file mode 100644 index 0000000000000000000000000000000000000000..f0c289c555cfd96c9947c9d71adea16f8a7ec384 GIT binary patch literal 999 zcmVJ6$5nhnYZ^#);scmwDJX(u2XB$%+)<6w^myF1uJ_y3zi1lS#{ zIoPj2lXDcj7IcwV;)tTzZCA(Qu%s2{+*%eQlp(J$=TsI2>mV6lcFW5bq=u5~onXgud)OpLqiMyb` zOO554W4;m#X-J9N#~q0l#d3W0OMF!pZf_S79 zNGKh31m(|y^w9(91#(4^b6)fwH=mXAJC=n!=>MvG_KA^*>e`i!eV>&&f&@$CZf42T zy&#W3g4WHfc7UskS*84uxAS9^p@pbtv<9n0lGDc9QS6lgQBk|pq@l#hxa!>~%0 zfOc_5CFxv=GiP0{k@+Hh^Ps7zh)dLk4>=>q^};c>NYUCAbyC$s>8LNZNYMmpl@l4# zP?KIHL&V9i_G;&fbkw93$tvefL)O8jv>z#ly_H_8e3ibptr*iNW(m$NZ9jtfkQdqI4yd<5dq75 zqG$})WEgB3MIuTr%vt?Mi%`#T>0KabFTklDUkEme*JK zw8{6X`;G`ZbT3-YM>1X%4Byk0!bHeqtLZ~A^gjhmX$=e64QpJGH{(bxVFDoe_?4~&SE>;tJEQ6!fAODasu%q=rB3o6r1Ey~<=V&c3Yi|;#g z@11+++}-=YfrWkFIWyw*k)sD}c{{Z-HNd ze}KKfPGB?eC9o3c1?~c-16{EPQ6>%rE&vt)8-e|R$tZ)sI$$9%6KIP)NR3PdUIw-Y zFt|(lfyJV;i4jZ-a6Ry80YvN3S%ztuL5J5lw$ALr_1<|J_2}8 zfdNe*`3M+INjMt#Sb@<@9kDNDyHk8`EieY<94j3)iUqlv9|U$K{H_gmj6?BCr%!=_ zO#`u4{(*E!jA<~T6V%-aZOW$|BeK3u|Ibf&@vPPKOt7n+R}~o7c+UTg77j}jw6zLo zMgb2iFs$*A0bsnxc;?DyecF7K$V^vYSmP;+5{&C=$u;=ACqxTfbO1joFs$*A4^q5L z&G2-B4WXYv1ePf>V%Q%Cm5YGoI1<@?I z*Vys<#5SSyZUx#5oyFNksZ)Y9433bF>e(c<;G2s zjc*E^b`D0bNB?fgvjbl#a6n8tq^hD(|N8Q(wwSEI_=fo0@6ksaTMEvL3XE@vPrJum z@Iq*%t&$^+ZG3|c7C8FYAG*Y4ngYWcxd=3Hsz-%K&=}EE3XE^0Tto}29esTwq_PbP zjK2do4L#iL=o<)FChT;I+VN% z^prf-la{xOhz&jGQ1Y03E|e3xDGv_?%)-^hRpEBy0iO-wgm%iqwT`jaEmgU%fNaS* z0iDoJc{tZGmM=mmaaA^-evt2Eal$Oh!-=xIEAC+{;t1%3z7jozM|&jfGvg{R zZxr)L>x6AGzaEKhS{&oBRTY`p>rmpVI0(5!H7V;s-vZ7cJgr4I;CF`-TaJ!$Cm0E3 z*f~2SokdJRt{m`5p6zIG!kdKUiTC_P!!S8Zh=jt-Q8d+F0td{> z*gyMV{nMetZvGC=RFA>gd0e{#`ZBIg9`VpIg&b&-Z8)yGpi8*}Oon{J@v;Z4QQI5y zJPdHQ$KbM70_-lx2{rjk?uuSn+c)ARkM?dRM_Q$Br%?w6GS*~aTa{?Lm0FH0tJO%lk~Tw69-NX=mr)>VG*)pR;}I(wIwdV>P~2OO z*v40RY-Npc(!nGH>X&DO?}oP6$J>WUJmt!ml&9zJkub#Bj`m|tJ4x@>sLzB|83wyn zlHL=6Lx!{+>w&9o&Ueu)=W1sC(T457JJOb`6flo<{(McVoit37;;mRyhsoghpo#UE z*mE^sS721*Da&Gp7xLsn;A;g2Hg#l^hR{OZjFO_&tqKfYlOH6RQo5fi$wI3faF^ue zEwuDW4mbBSqIM=qEqzfxWw=BR(iqV{WmqrWX-}GZQqJ26aZJvjyhJUKRZStM+YT2x zd1DNr290CE>BSOAm)a_7yP2PA=NfFeLCAPHo{ z84&dlDTo?~ERX~OxMBzwE&}HQ&B4kB3L;Cv)j$k{b1@lbAAbdT-nS&kFPMRmiJ6U^ zgG)?OT1HM@LsLsz-@wSh(K#qMBs45MA|f&>Ju@piC%1pX#7Rq+Enl%}+u>7ZE?m5H z`TC7JckkVQ@c7BI7q8!Z{_^en&tJb^&Wqy%8qApF?e4<(pGo%~kj>=j;us)rtQRJGsEa%H8g37qvLx4Qb#bhd^ZDQV`)b0oBBt4YJGS3)`NI#olBWJLe;)YG zoLTW>Q;53tzbL)u^U6w;Q(?xuxZw3E9)t@Me1So`X%r0(|r5hb?&KbChfU3WwJTlJ(W> YQ>N&$CpT=60R|0&r>mdKI;Vst055Vv0ssI2 literal 0 HcmV?d00001 diff --git a/src/shared/assets/images/forwardArrow.png b/src/shared/assets/images/forwardArrow.png new file mode 100644 index 0000000000000000000000000000000000000000..e191753ee6b244d597b13795717a731d85915e2e GIT binary patch literal 391 zcmeAS@N?(olHy`uVBq!ia0vp^Q6S901|%(3I5Gh#&H|6fVg?3oVGw3ym^DX&fq~J| z)5S5QV$Rz;ySVDkquwCD*;y=GdteUYu9!RE%-v^Rz zm*36Q`NlBy$*=Ys+dcc-WR~p~iheWg)TX0(D|Luw9RE1+oP2k!*(ZnJeZ~H!)hnTWn|RrWC@AlgD+FV|IPc zt8Dj-yYt9G?aQ^$naMFb`%8Kz$G&;{uIur4@5I getFormMessageOptions(loggedInUser: CometChat.User, messageObject: CometChat.BaseMessage, group: CometChat.Group): Array + getSchedulerMessageOptions(loggedInUser: CometChat.User, messageObject: CometChat.BaseMessage, group: CometChat.Group): Array getCardMessageOptions(loggedInUser: CometChat.User, messageObject: CometChat.BaseMessage, group: CometChat.Group): Array getAudioMessageOptions(loggedInUser: CometChat.User, messageObject: CometChat.BaseMessage, group: CometChat.Group): Array getVideoMessageOptions(loggedInUser: CometChat.User, messageObject: CometChat.BaseMessage, group: CometChat.Group): Array @@ -33,6 +36,7 @@ export interface DataSource { getVideoMessageBubble(videoUrl: string, thumbnailUrl: string, message: CometChat.MediaMessage, theme: CometChatTheme, videoBubbleStyle: VideoBubbleStyleInterface) getTextMessageBubble(messageText: string, message: CometChat.TextMessage, alignment: MessageBubbleAlignmentType, theme: CometChatTheme): JSX.Element getFormMessageBubble(message: FormMessage, theme: CometChatTheme, style?: FormBubbleStyle, onSubmitClick?: (data: any) => void): JSX.Element + getSchedulerMessageBubble(message: SchedulerMessage, theme: CometChatTheme, style?: SchedulerBubbleStyles, onScheduleClick?: (data: any) => void): JSX.Element getCardMessageBubble(message: CardMessage, theme: CometChatTheme, style?: CardBubbleStyle, onSubmitClick?: (data: any) => void): JSX.Element getImageMessageBubble(imageUrl: string,caption: string,style: ImageBubbleStyleInterface,message: CometChat.MediaMessage, theme: CometChatTheme): JSX.Element getAudioMessageBubble(audioUrl: string, title: string, style: AudioBubbleStyleInterface, message: CometChat.MediaMessage, theme: CometChatTheme): JSX.Element @@ -43,6 +47,7 @@ export interface DataSource { //content views getTextMessageContentView(message: CometChat.BaseMessage, alignment: MessageBubbleAlignmentType, theme: CometChatTheme): JSX.Element getFormMessageContentView(message: FormMessage, alignment: MessageBubbleAlignmentType, theme: CometChatTheme): JSX.Element + getSchedulerMessageContentView(message: SchedulerMessage, alignment: MessageBubbleAlignmentType, theme: CometChatTheme): JSX.Element getCardMessageContentView(message: CardMessage, alignment: MessageBubbleAlignmentType, theme: CometChatTheme): JSX.Element getAudioMessageContentView(message: CometChat.BaseMessage, alignment: MessageBubbleAlignmentType, theme: CometChatTheme): JSX.Element getVideoMessageContentView(message: CometChat.BaseMessage, alignment: MessageBubbleAlignmentType, theme: CometChatTheme): JSX.Element @@ -53,6 +58,7 @@ export interface DataSource { //templates getTextMessageTemplate(theme: CometChatTheme): CometChatMessageTemplate getFormMessageTemplate(theme: CometChatTheme): CometChatMessageTemplate + getSchedulerMessageTemplate(theme: CometChatTheme): CometChatMessageTemplate getCardMessageTemplate(theme: CometChatTheme): CometChatMessageTemplate getAudioMessageTemplate(theme: CometChatTheme): CometChatMessageTemplate getVideoMessageTemplate(theme: CometChatTheme): CometChatMessageTemplate diff --git a/src/shared/framework/DataSourceDecorator.tsx b/src/shared/framework/DataSourceDecorator.tsx index cb3a375..4e6e0cb 100644 --- a/src/shared/framework/DataSourceDecorator.tsx +++ b/src/shared/framework/DataSourceDecorator.tsx @@ -3,10 +3,12 @@ import { MessageBubbleAlignmentType } from "../constants/UIKitConstants"; import { CometChatMessageComposerActionInterface } from "../helper/types"; import { CometChatMessageOption, CometChatMessageTemplate } from "../modals"; import { CardMessage, FormMessage } from "../modals/InteractiveData"; +import { SchedulerMessage } from "../modals/InteractiveData/InteractiveMessage"; import { CometChatTheme } from "../resources/CometChatTheme"; import { VideoBubbleStyleInterface, ImageBubbleStyleInterface, AudioBubbleStyleInterface, FileBubbleStyleInterface } from "../views"; import { CardBubbleStyle } from "../views/CometChatCardBubble/CardBubbleStyle"; import { FormBubbleStyle } from "../views/CometChatFormBubble/FormBubbleStyle"; +import { SchedulerBubbleStyles } from "../views/CometChatSchedulerBubble"; import { DataSource } from "./DataSource"; import { CometChat } from "@cometchat/chat-sdk-react-native"; @@ -30,6 +32,10 @@ export class DataSourceDecorator implements DataSource { return this.dataSource.getFormMessageOptions(loggedInUser, messageObject, group) } + getSchedulerMessageOptions(loggedInUser: CometChat.User, messageObject: CometChat.BaseMessage, group: CometChat.Group): CometChatMessageOption[] { + return this.dataSource.getSchedulerMessageOptions(loggedInUser, messageObject, group) + } + getCardMessageOptions(loggedInUser: CometChat.User, messageObject: CometChat.BaseMessage, group: CometChat.Group): CometChatMessageOption[] { return this.dataSource.getCardMessageOptions(loggedInUser, messageObject, group) } @@ -77,6 +83,9 @@ export class DataSourceDecorator implements DataSource { getFormMessageBubble(message: FormMessage, theme: CometChatTheme, style?: FormBubbleStyle, onSubmitClick?: (data: any) => void) { return this.dataSource.getFormMessageBubble(message, theme, style, onSubmitClick); } + getSchedulerMessageBubble(message: SchedulerMessage, theme: CometChatTheme, style?: SchedulerBubbleStyles, onScheduleClick?: (data: any) => void) { + return this.dataSource.getSchedulerMessageBubble(message, theme, style, onScheduleClick); + } getCardMessageBubble(message: CardMessage, theme: CometChatTheme, style?: CardBubbleStyle, onSubmitClick?: (data: any) => void) { return this.dataSource.getCardMessageBubble(message, theme, style, onSubmitClick); @@ -106,6 +115,10 @@ export class DataSourceDecorator implements DataSource { return this.dataSource.getFormMessageContentView(message, alignment, theme); } + getSchedulerMessageContentView(message: CometChat.BaseMessage, alignment: MessageBubbleAlignmentType, theme: CometChatTheme) { + return this.dataSource.getSchedulerMessageContentView(message, alignment, theme); + } + getCardMessageContentView(message: CometChat.BaseMessage, alignment: MessageBubbleAlignmentType, theme: CometChatTheme) { return this.dataSource.getCardMessageContentView(message, alignment, theme); } @@ -134,6 +147,10 @@ export class DataSourceDecorator implements DataSource { return this.dataSource.getFormMessageTemplate(theme) } + getSchedulerMessageTemplate(theme: CometChatTheme): CometChatMessageTemplate { + return this.dataSource.getSchedulerMessageTemplate(theme) + } + getCardMessageTemplate(theme: CometChatTheme): CometChatMessageTemplate { return this.dataSource.getCardMessageTemplate(theme) } diff --git a/src/shared/framework/MessageDataSource.tsx b/src/shared/framework/MessageDataSource.tsx index cb42bba..7691255 100644 --- a/src/shared/framework/MessageDataSource.tsx +++ b/src/shared/framework/MessageDataSource.tsx @@ -21,6 +21,8 @@ import { CometChatFormBubble, CometChatCardBubble } from "../views"; import { CardMessage, FormMessage } from "../modals/InteractiveData"; import { FormBubbleStyle } from "../views/CometChatFormBubble/FormBubbleStyle"; import { CardBubbleStyle } from "../views/CometChatCardBubble/CardBubbleStyle"; +import { SchedulerMessage } from "../modals/InteractiveData/InteractiveMessage"; +import { CometChatSchedulerBubble, SchedulerBubbleStyles } from "../views/CometChatSchedulerBubble"; function isAudioMessage(message: CometChat.BaseMessage): message is CometChat.MediaMessage { return message.getCategory() == CometChat.CATEGORY_MESSAGE && @@ -152,6 +154,13 @@ export class MessageDataSource implements DataSource { return optionsList; } + getSchedulerMessageOptions(loggedInUser: CometChat.User, messageObject: CometChat.BaseMessage, group: CometChat.Group): CometChatMessageOption[] { + let optionsList: Array = []; + if (!isDeletedMessage(messageObject)) + optionsList.push(...ChatConfigurator.dataSource.getCommonOptions(loggedInUser, messageObject, group)); + return optionsList; + } + getCardMessageOptions(loggedInUser: CometChat.User, messageObject: CometChat.BaseMessage, group: CometChat.Group): CometChatMessageOption[] { let optionsList: Array = []; if (!isDeletedMessage(messageObject)) @@ -311,6 +320,15 @@ export class MessageDataSource implements DataSource { style={style} /> } + + getSchedulerMessageBubble(message: SchedulerMessage, theme: CometChatTheme, style?: SchedulerBubbleStyles, onScheduleClick?: (data: any) => void): JSX.Element { + return + } + getCardMessageBubble(message: CardMessage, theme: CometChatTheme, style?: CardBubbleStyle, onSubmitClick?: (data: any) => void): JSX.Element { return { + if (isDeletedMessage(message)) { + return ChatConfigurator.dataSource.getDeleteMessageBubble(message, theme); + } else { + return ChatConfigurator.dataSource.getSchedulerMessageContentView(message, _alignment, theme); + } + }, + options: (loggedInuser, message, group) => ChatConfigurator.dataSource.getSchedulerMessageOptions(loggedInuser, message, group), + }); + } + getCardMessageTemplate(theme: CometChatTheme): CometChatMessageTemplate { return new CometChatMessageTemplate({ type: MessageTypeConstants.card, @@ -520,6 +556,7 @@ export class MessageDataSource implements DataSource { return [ ChatConfigurator.dataSource.getTextMessageTemplate(theme), ChatConfigurator.dataSource.getFormMessageTemplate(theme), + ChatConfigurator.dataSource.getSchedulerMessageTemplate(theme), ChatConfigurator.dataSource.getCardMessageTemplate(theme), ChatConfigurator.dataSource.getAudioMessageTemplate(theme), ChatConfigurator.dataSource.getVideoMessageTemplate(theme), @@ -556,6 +593,9 @@ export class MessageDataSource implements DataSource { case MessageTypeConstants.form: template = ChatConfigurator.dataSource.getFormMessageTemplate(theme) break; + case MessageTypeConstants.scheduler: + template = ChatConfigurator.dataSource.getSchedulerMessageTemplate(theme) + break; case MessageTypeConstants.card: template = ChatConfigurator.dataSource.getCardMessageTemplate(theme) break; @@ -573,7 +613,8 @@ export class MessageDataSource implements DataSource { MessageTypeConstants.groupActions, MessageTypeConstants.groupMember, MessageTypeConstants.form, - MessageTypeConstants.card + MessageTypeConstants.card, + MessageTypeConstants.scheduler ]; } getAllMessageCategories(): string[] { diff --git a/src/shared/index.ts b/src/shared/index.ts index b7d99e0..cdcedc5 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -38,6 +38,7 @@ import { DropdownElement, ElementEntity, FormMessage, + SchedulerMessage, LabelElement, OptionElement, RadioButtonElement, @@ -136,7 +137,9 @@ import { CometChatFormBubble, CometChatFormBubbleInterface, CometChatCardBubble, - CometChatCardBubbleInterface + CometChatCardBubbleInterface, + CometChatSchedulerBubble, + CometChatSchedulerBubbleInterface } from './views'; import { @@ -289,6 +292,8 @@ export { CometChatFormBubbleInterface, CometChatCardBubble, CometChatCardBubbleInterface, + CometChatSchedulerBubble, + CometChatSchedulerBubbleInterface, APIAction, ActionEntity, BaseInputElement, @@ -301,6 +306,7 @@ export { DropdownElement, ElementEntity, FormMessage, + SchedulerMessage, LabelElement, OptionElement, RadioButtonElement, diff --git a/src/shared/libs/CometChatCalendar/Calendars/BaseCalendar.tsx b/src/shared/libs/CometChatCalendar/Calendars/BaseCalendar.tsx new file mode 100644 index 0000000..f2456b1 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Calendars/BaseCalendar.tsx @@ -0,0 +1,226 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { View } from 'react-native'; +import dayjs from 'dayjs'; + +import localeData from 'dayjs/plugin/localeData'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; +import utc from 'dayjs/plugin/utc'; + +import { getSurroundingTimeUnits } from '../Utils'; +import { useSurroundingTimeUnits } from '../Hooks'; +import { ThemeContext, LocaleContext } from '../Contexts'; +import { VIEW } from '../Constants'; + +import { + Months, + Days, + Arrow as DefaultArrow, + Title as DefaultTitle, + Weekdays as DefaultWeekdays, +} from '../Components'; + +import type { + Theme, + Locale, + ArrowComponentType, + TitleComponentType, + DayComponentType, + MonthComponentType, + WeekdaysComponentType, +} from '../Entities'; + +dayjs.extend(localeData); +dayjs.extend(localizedFormat); +dayjs.extend(utc); + +export interface Props { + ArrowComponent?: ArrowComponentType; + TitleComponent?: TitleComponentType; + DayComponent?: DayComponentType; + MonthComponent?: MonthComponentType; + WeekdaysComponent?: WeekdaysComponentType; + testID?: string; + showExtraDates?: boolean; + allowYearView?: boolean; // Boolean that disables the calendar year view + onPressDay: (date: string) => void; // Recieves a date format string + minDate?: string; // YYYY-MM-DD format string respresenting the minimum date that can be selected + maxDate?: string; // YYYY-MM-DD format string respresenting the maximum date that can be selected + initVisibleDate?: string; // YYYY-MM-DD format string respresenting the date that should be visible when the calendar first renders + dateProperties: { + [date: string /* YYYY-MM-DD */]: { + isSelected?: boolean; + }; + }; +} + +const BaseCalendar: React.FC = ({ + ArrowComponent: CustomArrow, + TitleComponent: CustomTitle, + WeekdaysComponent: CustomWeekdays, + DayComponent, + MonthComponent, + allowYearView, + showExtraDates, + onPressDay, + maxDate, + minDate, + initVisibleDate, + dateProperties, + testID, +}) => { + const theme = React.useContext(ThemeContext); + const locale = React.useContext(LocaleContext); + + const [rightArrowDisabled, disableRightArrow] = useState(false); + const [leftArrowDisabled, disableLeftArrow] = useState(false); + const [activeView, setActiveView] = useState(VIEW.MONTH); + const [visibleDate, setVisibleDate] = useState( + initVisibleDate ?? dayjs().local().format() + ); + + const localeAwareVisibleDate = React.useMemo( + () => dayjs(visibleDate).locale(locale.name, locale), + [locale, visibleDate] + ); + + const weekdays = React.useMemo(() => locale.weekdaysShort ?? [''], [locale]); + + const { month, year } = useSurroundingTimeUnits(visibleDate); + + const verifyUnitIsPastMaxDate = useCallback( + (unit) => { + if (maxDate) { + if (unit.isAfter(maxDate)) { + disableRightArrow(true); + } else { + disableRightArrow(false); + } + } + }, + [disableRightArrow, maxDate] + ); + + const verifyUnitIsBeforeMinDate = useCallback( + (unit) => { + if (minDate) { + if (unit.isBefore(minDate)) { + disableLeftArrow(true); + } else { + disableLeftArrow(false); + } + } + }, + [disableLeftArrow, minDate] + ); + + const toggleCalendarView = useCallback( + (newVisibleDate: string = visibleDate) => { + const { month: _month, year: _year } = getSurroundingTimeUnits(newVisibleDate); + + if (activeView === VIEW.MONTH) { + setActiveView(VIEW.YEAR); + verifyUnitIsPastMaxDate(_year.next); + verifyUnitIsBeforeMinDate(_year.last); + } + + if (activeView === VIEW.YEAR) { + setActiveView(VIEW.MONTH); + verifyUnitIsPastMaxDate(_month.next); + verifyUnitIsBeforeMinDate(_month.last); + } + }, + [activeView, verifyUnitIsBeforeMinDate, verifyUnitIsPastMaxDate, visibleDate] + ); + + useEffect(() => { + verifyUnitIsPastMaxDate(month.next.start); + verifyUnitIsBeforeMinDate(month.last.end); + }, [ + month.last.end, + month.next.start, + verifyUnitIsPastMaxDate, + verifyUnitIsBeforeMinDate, + ]); + + const addMonth = useCallback(() => { + setVisibleDate(month.next.start.local().format()); + verifyUnitIsPastMaxDate(month.afterNext); + verifyUnitIsBeforeMinDate(month.current.end); + }, [month, verifyUnitIsBeforeMinDate, verifyUnitIsPastMaxDate]); + + const subtractMonth = useCallback(() => { + setVisibleDate(month.last.start.local().format()); + verifyUnitIsPastMaxDate(month.current.start); + verifyUnitIsBeforeMinDate(month.beforeLast); + }, [month, verifyUnitIsBeforeMinDate, verifyUnitIsPastMaxDate]); + + const addYear = useCallback(() => { + setVisibleDate(year.next.persistMonth.local().format()); + verifyUnitIsPastMaxDate(year.afterNext); + verifyUnitIsBeforeMinDate(year.current.end); + }, [year, verifyUnitIsBeforeMinDate, verifyUnitIsPastMaxDate]); + + const subtractYear = useCallback(() => { + setVisibleDate(year.last.persistMonth.local().format()); + verifyUnitIsPastMaxDate(year.current.start); + verifyUnitIsBeforeMinDate(year.beforeLast); + }, [year, verifyUnitIsBeforeMinDate, verifyUnitIsPastMaxDate]); + + const onPressMonth = (newVisibleDate: string) => { + setVisibleDate(newVisibleDate); + toggleCalendarView(newVisibleDate); + }; + + const Weekdays = CustomWeekdays || DefaultWeekdays; + const Arrow = CustomArrow || DefaultArrow; + const Title = CustomTitle || DefaultTitle; + + return ( + + + + + <Arrow + direction={'right'} + isDisabled={rightArrowDisabled} + onPress={activeView === VIEW.MONTH ? addMonth : addYear} + /> + </View> + {activeView === VIEW.MONTH ? ( + <> + <Weekdays days={weekdays} /> + <Days + DayComponent={DayComponent} + visibleDate={localeAwareVisibleDate} + showExtraDates={!!showExtraDates} + dateProperties={dateProperties} + onPressDay={onPressDay} + minDate={minDate} + maxDate={maxDate} + /> + </> + ) : ( + <Months + MonthComponent={MonthComponent} + visibleDate={localeAwareVisibleDate} + minDate={minDate ? dayjs(minDate).local().format() : undefined} + maxDate={maxDate ? dayjs(maxDate).local().format() : undefined} + dateProperties={dateProperties} + onPressMonth={onPressMonth} + /> + )} + </View> + ); +}; + +export default BaseCalendar; diff --git a/src/shared/libs/CometChatCalendar/Calendars/DateSelectionCalendar.tsx b/src/shared/libs/CometChatCalendar/Calendars/DateSelectionCalendar.tsx new file mode 100644 index 0000000..b2bad47 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Calendars/DateSelectionCalendar.tsx @@ -0,0 +1,88 @@ +import React, { useMemo } from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +import BaseCalendarWrappedInProviders from './Providers'; +import type { DateProperties, WrapperCalendarProps } from '../Entities'; + +dayjs.extend(utc); + +interface SpecificProps { + onSelectDate: (date: string) => void; + selectedDate: string; +} + +type Props = SpecificProps & WrapperCalendarProps; + +// A thin wrapper to limit the props that can be passed to the BaseCalendar component +const DateSelectionCalendar: React.FC<Props> = ({ + onSelectDate, + disabledDates, + selectedDate, + initVisibleDate, + allowYearView = true, + ...others +}) => { + if (!selectedDate) { + throw new Error( + 'The `selectedDate` prop is required. Use an empty array if no dates should be selected.' + ); + } + + if (typeof selectedDate !== 'string') { + throw new Error( + 'The `selectedDate` prop should be a date string in YYYY-MM-DD format.' + ); + } + + if (!onSelectDate) { + throw new Error('The `onSelectDate` prop is required.'); + } + + if (typeof onSelectDate !== 'function') { + throw new Error( + 'The `onSelectDate` prop should be function that receives a date string as paramater.' + ); + } + + const dateProperties = useMemo(() => { + let disabledDateProperties: Record<string, DateProperties> = {}; + let selectedDateProperties: Record<string, DateProperties> = {}; + + disabledDateProperties = (disabledDates as string[])?.reduce( + (disabled: Record<string, DateProperties>, date) => { + disabled[date] = { isDisabled: true }; + return disabled; + }, + {} + ); + + if (dayjs(selectedDate).isValid()) { + selectedDateProperties = { + [dayjs(selectedDate).local().format('YYYY-MM-DD')]: { + isSelected: true, + }, + }; + } + + // Not possible for a date to be both disabled and selected, so overwriting is OK + return { + ...disabledDateProperties, + ...selectedDateProperties, + }; + }, [selectedDate, disabledDates]); + + return ( + <BaseCalendarWrappedInProviders + allowYearView={allowYearView} + onPressDay={onSelectDate} + initVisibleDate={ + initVisibleDate || (dayjs(selectedDate).isValid() ? selectedDate : undefined) + } + dateProperties={dateProperties} + {...others} + /> + ); +}; + +export default DateSelectionCalendar; diff --git a/src/shared/libs/CometChatCalendar/Calendars/MultiDateSelectionCalendar.tsx b/src/shared/libs/CometChatCalendar/Calendars/MultiDateSelectionCalendar.tsx new file mode 100644 index 0000000..e3027c1 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Calendars/MultiDateSelectionCalendar.tsx @@ -0,0 +1,106 @@ +import React, { useMemo } from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +import BaseCalendarWrappedInProviders from './Providers'; +import type { DateProperties, WrapperCalendarProps } from '../Entities'; + +dayjs.extend(utc); + +interface SpecificProps { + onSelectDates: (dates: string[]) => void; + selectedDates: string[]; +} + +type Props = SpecificProps & WrapperCalendarProps; + +// A thin wrapper to limit the props that can be passed to the BaseCalendar component +const MultiDateSelectionCalendar: React.FC<Props> = ({ + onSelectDates, + disabledDates, + selectedDates, + allowYearView = true, + ...others +}) => { + if (!selectedDates) { + throw new Error( + 'The `selectedDates` prop is required. Use an empty array if no dates should be selected.' + ); + } + + if (typeof selectedDates !== 'object') { + throw new Error( + 'The `selectedDates` prop should be an array of date strings in YYYY-MM-DD format.' + ); + } + + if (!onSelectDates) { + throw new Error('The `onSelectDates` prop is required.'); + } + + if (typeof onSelectDates !== 'function') { + throw new Error( + 'The `onSelectDates` prop should be function that receives an array of date strings as paramater.' + ); + } + + const selDatesRef = React.useRef<string[]>(selectedDates); + + const dateProperties = useMemo(() => { + const disabledDateProperties = disabledDates?.reduce( + (disabled: Record<string, DateProperties>, date) => { + disabled[date] = { isDisabled: true }; + return disabled; + }, + {} + ); + + const selectedDateProperties = selectedDates.reduce( + (selected: Record<string, DateProperties>, date) => { + selected[date] = { isSelected: true }; + return selected; + }, + {} + ); + + // Not possible for a date to be both disabled and selected, so overwriting is OK + return { + ...disabledDateProperties, + ...selectedDateProperties, + }; + }, [selectedDates, disabledDates]); + + const remove = (dateToRemove: string) => { + const newSelectedDates = selDatesRef.current.filter((date) => date !== dateToRemove); + selDatesRef.current = newSelectedDates; + return newSelectedDates; + }; + + const append = (dateToAppend: string) => { + const newSelectedDates = [...selDatesRef.current, dateToAppend].sort(); + selDatesRef.current = newSelectedDates; + return newSelectedDates; + }; + + const onPressDay = React.useCallback( + (date: string) => { + if (selDatesRef.current.includes(date)) { + onSelectDates(remove(date)); + } else { + onSelectDates(append(date)); + } + }, + [onSelectDates] + ); + + return ( + <BaseCalendarWrappedInProviders + onPressDay={onPressDay} + allowYearView={allowYearView} + dateProperties={dateProperties} + {...others} + /> + ); +}; + +export default MultiDateSelectionCalendar; diff --git a/src/shared/libs/CometChatCalendar/Calendars/Providers.tsx b/src/shared/libs/CometChatCalendar/Calendars/Providers.tsx new file mode 100644 index 0000000..3b44353 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Calendars/Providers.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { ThemeContext, LocaleContext } from '../Contexts'; +import { DefaultTheme } from '../Themes'; +import { DefaultLocale } from '../Locales'; +import type { Theme, Locale } from '../Entities'; + +import BaseCalendar, { Props as BaseCalendarProps } from './BaseCalendar'; + +interface ProviderProps { + locale?: Locale; + theme?: Theme; +} + +type Props = ProviderProps & BaseCalendarProps; + +const Providers: React.FC<Props> = ({ theme, locale, ...otherProps }) => { + return ( + <LocaleContext.Provider value={locale ?? DefaultLocale}> + <ThemeContext.Provider value={theme ?? DefaultTheme}> + <BaseCalendar {...otherProps} /> + </ThemeContext.Provider> + </LocaleContext.Provider> + ); +}; + +export default React.memo(Providers); diff --git a/src/shared/libs/CometChatCalendar/Calendars/index.ts b/src/shared/libs/CometChatCalendar/Calendars/index.ts new file mode 100644 index 0000000..4c599e2 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Calendars/index.ts @@ -0,0 +1,3 @@ +export { default as DateSelectionCalendar } from './DateSelectionCalendar'; +export { default as SingleDateSelectionCalendar } from './DateSelectionCalendar'; +export { default as MultiDateSelectionCalendar } from './MultiDateSelectionCalendar'; diff --git a/src/shared/libs/CometChatCalendar/Components/Arrow.test.tsx b/src/shared/libs/CometChatCalendar/Components/Arrow.test.tsx new file mode 100644 index 0000000..dafbde8 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Arrow.test.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import type { ReactTestInstance } from 'react-test-renderer'; +import { render, fireEvent } from '@testing-library/react-native'; +import { ThemeContext } from '../Contexts'; +import { DefaultTheme } from '../Themes'; + +import Arrow, { Props } from './Arrow'; + +test('Arrow renders without error', () => { + const arrow = new ArrowPage({ direction: 'left' }); + expect(arrow.component).toBeTruthy(); +}); + +test('Component calls onPress callback when clicked', () => { + const onPress = jest.fn(); + const month = new ArrowPage({ onPress }); + fireEvent.press(month.component); + expect(onPress).toHaveBeenCalledTimes(1); +}); + +describe('Enabling and disabling', () => { + test('Disable clicking if prop ´isDisabled´ is true', () => { + const arrow = new ArrowPage({ isDisabled: true }); + expect(arrow.component).toBeDisabled(); + }); + + test('Enable clicking if prop ´isDisabled´ is false', () => { + const arrow = new ArrowPage({ isDisabled: false }); + expect(arrow.component).toBeEnabled(); + }); +}); + +describe('Theme context', () => { + test('Container applies normal theme in enabled state', () => { + const arrow = new ArrowPage({}); + expect(arrow.component).toHaveStyle(theme.normalArrowContainer); + expect(arrow.component).not.toHaveStyle(theme.disabledArrowContainer); + }); + + test('Container applies isDisabled theme in isDisabled state', () => { + const arrow = new ArrowPage({ isDisabled: true }); + expect(arrow.component).toHaveStyle([ + theme.normalArrowContainer, + theme.disabledArrowContainer, + ]); + }); + + test('Image applies normal theme in enabled state', () => { + const arrow = new ArrowPage({}); + expect(arrow.image).toHaveStyle(theme.normalArrowImage); + expect(arrow.image).not.toHaveStyle(theme.disabledArrowImage); + }); + + test('Image applies isDisabled theme in isDisabled state', () => { + const arrow = new ArrowPage({ isDisabled: true }); + expect(arrow.image).toHaveStyle([theme.normalArrowImage, theme.disabledArrowImage]); + }); +}); + +describe('Automatic image rotation', () => { + const rotation = { transform: [{ rotate: '180deg' }] }; + + test('Rotate "right" arrow towards the right', () => { + const arrow = new ArrowPage({ direction: 'right' }); + expect(arrow.image).toHaveStyle(rotation); + }); + + test('Do not rotate "left" arrows', () => { + const arrow = new ArrowPage({ direction: 'left' }); + expect(arrow.image).not.toHaveStyle(rotation); + }); +}); + +class ArrowPage { + component: ReactTestInstance; + image: ReactTestInstance; + + constructor({ + direction = 'left', + isDisabled = false, + onPress = () => {}, + }: Partial<Props>) { + const { getByLabelText, getByTestId } = render( + <ThemeContext.Provider value={theme}> + <Arrow {...{ direction, isDisabled, onPress }} /> + </ThemeContext.Provider> + ); + + this.component = getByLabelText(`${direction} arrow`); + this.image = getByTestId('arrow-image'); + } +} + +const theme = { + ...DefaultTheme, + normalArrowContainer: { + marginTop: 10, + backgroundColor: 'green', + }, + disabledArrowContainer: { + marginBottom: 10, + backgroundColor: 'gray', + }, + normalArrowImage: { + marginTop: 10, + tintColor: 'green', + }, + disabledArrowImage: { + marginBottom: 10, + tintColor: 'gray', + }, +}; diff --git a/src/shared/libs/CometChatCalendar/Components/Arrow.tsx b/src/shared/libs/CometChatCalendar/Components/Arrow.tsx new file mode 100644 index 0000000..f480383 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Arrow.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Image, TouchableOpacity, StyleSheet } from 'react-native'; + +import Icons from '../Icons'; +import { ThemeContext } from '../Contexts'; +import type { Theme } from '../Entities'; + +export interface Props { + direction: 'left' | 'right'; + isDisabled?: boolean; + onPress: () => void; +} + +const Arrow: React.FC<Props> = ({ direction, isDisabled, onPress }) => { + const theme = React.useContext<Theme>(ThemeContext); + + return ( + <TouchableOpacity + hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }} + accessibilityLabel={`${direction} arrow`} + accessibilityHint={'Press to move to previous month or year'} + accessibilityState={{ disabled: isDisabled }} + disabled={isDisabled} + onPress={onPress} + style={[theme.normalArrowContainer, isDisabled && theme.disabledArrowContainer]}> + <Image + testID={'arrow-image'} + accessibilityIgnoresInvertColors + source={Icons.arrow.left['16px']} + style={[ + theme.normalArrowImage, + isDisabled && theme.disabledArrowImage, + direction === 'right' && styles.right, + ]} + /> + </TouchableOpacity> + ); +}; + +export default React.memo(Arrow); + +const styles = StyleSheet.create({ + right: { + transform: [{ rotate: '180deg' }], + }, +}); diff --git a/src/shared/libs/CometChatCalendar/Components/Day.test.tsx b/src/shared/libs/CometChatCalendar/Components/Day.test.tsx new file mode 100644 index 0000000..b7bc9ef --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Day.test.tsx @@ -0,0 +1,396 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +import type { ReactTestInstance } from 'react-test-renderer'; +import { render, fireEvent } from '@testing-library/react-native'; +import { ThemeContext } from '../Contexts'; +import { DefaultTheme } from '../Themes'; + +import Day, { Props } from './Day'; + +dayjs.extend(utc); + +test('Month renders without error', () => { + const day = new DayPage({}); + expect(day.container).toBeTruthy(); +}); + +test('Component calls onPress callback when clicked', () => { + const onPress = jest.fn(); + const day = new DayPage({ onPress }); + day.container && fireEvent.press(day.container); + expect(onPress).toHaveBeenCalledTimes(1); +}); + +describe('Renders the correct string for date', () => { + test('Valid date', () => { + const date = '2020-09-18'; + const day = new DayPage({ date }); + expect(day.text).toHaveTextContent('18'); + }); + + test('Invalid date', () => { + console.warn = jest.fn(); + const date = '18-09-2020'; + const day = new DayPage({ date }); + expect(day.text).toHaveTextContent(''); + }); +}); + +describe('Extra dates', () => { + test('Hide extra dates', () => { + const date = '2020-01-10'; + const day = new DayPage({ date, isExtraDay: true, showExtraDates: false }); + expect(day.container).toBeFalsy(); + expect(day.text).toBeFalsy(); + expect(day.extraDayContainer).toBeTruthy(); + expect(day.extraDayText).toHaveTextContent(''); + }); + + test('Show extra dates', () => { + const date = '2020-01-10'; + const day = new DayPage({ date, isExtraDay: true, showExtraDates: true }); + expect(day.container).toBeFalsy(); + expect(day.text).toBeFalsy(); + expect(day.extraDayContainer).toBeTruthy(); + expect(day.extraDayText).toHaveTextContent('10'); + }); +}); + +describe('Enabling and disabling', () => { + test('Disable clicking if prop ´isDisabled´ is true', () => { + const day = new DayPage({ isDisabled: true }); + expect(day.container).toBeDisabled(); + }); + + test('Enable clicking if prop ´isDisabled´ is false', () => { + const day = new DayPage({ isDisabled: false }); + expect(day.container).toBeEnabled(); + }); +}); + +describe('Theme context', () => { + describe('Container', () => { + test('Container applies normal theme in enabled state', () => { + const day = new DayPage({}); + expect(day.container).toHaveStyle(theme.normalDayContainer); + + expect(day.container).not.toHaveStyle(theme.disabledDayContainer); + expect(day.container).not.toHaveStyle(theme.selectedDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.extraDayContainer); + }); + + test('Container applies disabled theme in disabled state', () => { + const day = new DayPage({ isDisabled: true }); + expect(day.container).toHaveStyle([ + theme.normalDayContainer, + theme.disabledDayContainer, + ]); + + expect(day.container).not.toHaveStyle(theme.selectedDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.extraDayContainer); + }); + + test('Container applies selected theme in selected state', () => { + const day = new DayPage({ isSelected: true }); + expect(day.container).toHaveStyle([ + theme.normalDayContainer, + theme.selectedDayContainer, + ]); + + expect(day.container).not.toHaveStyle(theme.disabledDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.extraDayContainer); + }); + + test('Container applies start of week themes in start of week state', () => { + const day = new DayPage({ isStartOfWeek: true }); + expect(day.container).toHaveStyle([ + theme.normalDayContainer, + theme.startOfWeekDayContainer, + ]); + + expect(day.container).not.toHaveStyle(theme.selectedDayContainer); + expect(day.container).not.toHaveStyle(theme.disabledDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.extraDayContainer); + }); + + test('Container applies end of week themes in end of week state', () => { + const day = new DayPage({ isEndOfWeek: true }); + expect(day.container).toHaveStyle([ + theme.normalDayContainer, + theme.endOfWeekDayContainer, + ]); + + expect(day.container).not.toHaveStyle(theme.selectedDayContainer); + expect(day.container).not.toHaveStyle(theme.disabledDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.extraDayContainer); + }); + + test('Container applies start of month themes in start of month state', () => { + const day = new DayPage({ isStartOfMonth: true }); + expect(day.container).toHaveStyle([ + theme.normalDayContainer, + theme.startOfMonthDayContainer, + ]); + + expect(day.container).not.toHaveStyle(theme.selectedDayContainer); + expect(day.container).not.toHaveStyle(theme.disabledDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.extraDayContainer); + }); + + test('Container applies end of month themes in end of month state', () => { + const day = new DayPage({ isEndOfMonth: true }); + expect(day.container).toHaveStyle([ + theme.normalDayContainer, + theme.endOfMonthDayContainer, + ]); + + expect(day.container).not.toHaveStyle(theme.selectedDayContainer); + expect(day.container).not.toHaveStyle(theme.disabledDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.startOfMonthDayContainer); + expect(day.container).not.toHaveStyle(theme.endOfWeekDayContainer); + expect(day.container).not.toHaveStyle(theme.extraDayContainer); + }); + + test('Container applies extra day themes in extra day state', () => { + const day = new DayPage({ isExtraDay: true, showExtraDates: true }); + + expect(day.container).toBeFalsy(); + expect(day.extraDayContainer).toHaveStyle([ + theme.normalDayContainer, + theme.extraDayContainer, + ]); + + expect(day.extraDayContainer).not.toHaveStyle(theme.selectedDayContainer); + expect(day.extraDayContainer).not.toHaveStyle(theme.disabledDayContainer); + expect(day.extraDayContainer).not.toHaveStyle(theme.startOfWeekDayContainer); + expect(day.extraDayContainer).not.toHaveStyle(theme.startOfMonthDayContainer); + expect(day.extraDayContainer).not.toHaveStyle(theme.endOfWeekDayContainer); + expect(day.extraDayContainer).not.toHaveStyle(theme.endOfMonthDayContainer); + }); + }); + + describe('Text', () => { + test('Text applies normal theme in enabled state', () => { + const day = new DayPage({}); + expect(day.text).toHaveStyle(theme.normalDayText); + + expect(day.text).not.toHaveStyle(theme.disabledDayText); + expect(day.text).not.toHaveStyle(theme.selectedDayText); + expect(day.text).not.toHaveStyle(theme.startOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.endOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.startOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.endOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.extraDayText); + }); + + test('Text applies disabled theme in disabled state', () => { + const day = new DayPage({ isDisabled: true }); + expect(day.text).toHaveStyle([theme.normalDayText, theme.disabledDayText]); + + expect(day.text).not.toHaveStyle(theme.selectedDayText); + expect(day.text).not.toHaveStyle(theme.startOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.endOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.startOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.endOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.extraDayText); + }); + + test('Text applies selected theme in selected state', () => { + const day = new DayPage({ isSelected: true }); + expect(day.text).toHaveStyle([theme.normalDayText, theme.selectedDayText]); + + expect(day.text).not.toHaveStyle(theme.disabledDayText); + expect(day.text).not.toHaveStyle(theme.startOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.endOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.startOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.endOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.extraDayText); + }); + + test('Text applies start of week themes in start of week state', () => { + const day = new DayPage({ isStartOfWeek: true }); + expect(day.text).toHaveStyle([theme.normalDayText, theme.startOfWeekDayText]); + + expect(day.text).not.toHaveStyle(theme.selectedDayText); + expect(day.text).not.toHaveStyle(theme.disabledDayText); + expect(day.text).not.toHaveStyle(theme.endOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.startOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.endOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.extraDayText); + }); + + test('Text applies end of week themes in end of week state', () => { + const day = new DayPage({ isEndOfWeek: true }); + expect(day.text).toHaveStyle([theme.normalDayText, theme.endOfWeekDayText]); + + expect(day.text).not.toHaveStyle(theme.selectedDayText); + expect(day.text).not.toHaveStyle(theme.disabledDayText); + expect(day.text).not.toHaveStyle(theme.startOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.startOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.endOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.extraDayText); + }); + + test('Text applies start of month themes in start of month state', () => { + const day = new DayPage({ isStartOfMonth: true }); + expect(day.text).toHaveStyle([theme.normalDayText, theme.startOfMonthDayText]); + + expect(day.text).not.toHaveStyle(theme.selectedDayText); + expect(day.text).not.toHaveStyle(theme.disabledDayText); + expect(day.text).not.toHaveStyle(theme.endOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.startOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.endOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.extraDayText); + }); + + test('Text applies end of month themes in end of month state', () => { + const day = new DayPage({ isEndOfMonth: true }); + expect(day.text).toHaveStyle([theme.normalDayText, theme.endOfMonthDayText]); + + expect(day.text).not.toHaveStyle(theme.selectedDayText); + expect(day.text).not.toHaveStyle(theme.disabledDayText); + expect(day.text).not.toHaveStyle(theme.startOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.startOfMonthDayText); + expect(day.text).not.toHaveStyle(theme.endOfWeekDayText); + expect(day.text).not.toHaveStyle(theme.extraDayText); + }); + + test('Text applies extra day themes in extra day state', () => { + const day = new DayPage({ isExtraDay: true, showExtraDates: true }); + + expect(day.text).toBeFalsy(); + expect(day.extraDayText).toHaveStyle([theme.normalDayText, theme.extraDayText]); + + expect(day.extraDayText).not.toHaveStyle(theme.selectedDayText); + expect(day.extraDayText).not.toHaveStyle(theme.disabledDayText); + expect(day.extraDayText).not.toHaveStyle(theme.startOfWeekDayText); + expect(day.extraDayText).not.toHaveStyle(theme.startOfMonthDayText); + expect(day.extraDayText).not.toHaveStyle(theme.endOfWeekDayText); + expect(day.extraDayText).not.toHaveStyle(theme.endOfMonthDayText); + }); + }); +}); + +class DayPage { + container: ReactTestInstance | null; + extraDayContainer: ReactTestInstance | null; + text: ReactTestInstance | null; + extraDayText: ReactTestInstance | null; + + constructor({ + date = '2020-01-01', + onPress = () => {}, + ...booleanProps + }: Partial<Props>) { + const { queryByTestId } = render( + <ThemeContext.Provider value={theme}> + <Day {...booleanProps} date={date} onPress={onPress} /> + </ThemeContext.Provider> + ); + + this.container = queryByTestId('day-container'); + this.extraDayContainer = queryByTestId('extra-day-container'); + this.text = queryByTestId('day-text'); + this.extraDayText = queryByTestId('extra-day-text'); + } +} + +const containerStyles = { + normalDayContainer: { + marginTop: 2, + marginLeft: 1, + }, + disabledDayContainer: { + marginTop: 3, + marginBottom: 1, + }, + selectedDayContainer: { + marginTop: 4, + paddingTop: 1, + }, + startOfWeekDayContainer: { + marginTop: 7, + paddingRight: 1, + }, + endOfWeekDayContainer: { + marginTop: 8, + backgroundColor: 'red', + }, + startOfMonthDayContainer: { + marginTop: 9, + elevation: 1, + }, + endOfMonthDayContainer: { + marginTop: 1, + alignItems: 'center' as const, + }, + extraDayContainer: { + marginTop: 10, + borderRadius: 100, + }, +}; + +const textStyle = { + normalDayText: { + marginTop: 2, + marginLeft: 1, + }, + disabledDayText: { + marginTop: 3, + marginBottom: 1, + }, + selectedDayText: { + marginTop: 4, + paddingTop: 1, + }, + startOfWeekDayText: { + marginTop: 7, + paddingRight: 1, + }, + endOfWeekDayText: { + marginTop: 8, + backgroundColor: 'red', + }, + startOfMonthDayText: { + marginTop: 9, + elevation: 1, + }, + endOfMonthDayText: { + marginTop: 1, + alignItems: 'center' as const, + }, + extraDayText: { + marginTop: 10, + color: 'purple', + }, +}; + +const theme = { + ...DefaultTheme, + ...containerStyles, + ...textStyle, +}; diff --git a/src/shared/libs/CometChatCalendar/Components/Day.tsx b/src/shared/libs/CometChatCalendar/Components/Day.tsx new file mode 100644 index 0000000..c15e3c2 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Day.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; + +import { Text, TouchableOpacity } from 'react-native'; +import type { DateProperties, Theme } from '../Entities'; +import { ThemeContext } from '../Contexts'; + +dayjs.extend(localizedFormat); + +interface OtherProps { + date: string; + onPress: (date: string) => void; + isStartOfWeek?: boolean; + isEndOfWeek?: boolean; + isStartOfMonth?: boolean; + isEndOfMonth?: boolean; + isExtraDay?: boolean; + showExtraDates?: boolean; +} + +export type Props = DateProperties & OtherProps; + +const Day: React.FC<Props> = ({ + date, + onPress, + showExtraDates = false, + isDisabled = false, + isSelected = false, + isStartOfWeek = false, + isEndOfWeek = false, + isStartOfMonth = false, + isEndOfMonth = false, + isExtraDay = false, +}) => { + const theme = React.useContext<Theme>(ThemeContext); + const _onPress = () => { + onPress(date); + }; + + if (isExtraDay) { + return ( + <TouchableOpacity + disabled + testID={'extra-day-container'} + accessibilityLabel={`${dayjs(date).format('LL')}`} + accessibilityState={{ disabled: true, selected: false }} + onPress={_onPress} + style={[theme.normalDayContainer, theme.extraDayContainer]}> + <Text testID={'extra-day-text'} style={[theme.normalDayText, theme.extraDayText]}> + {showExtraDates && dayjs(date).date()} + </Text> + </TouchableOpacity> + ); + } + + return ( + <TouchableOpacity + testID={'day-container'} + accessibilityLabel={`${dayjs(date).format('LL')}`} + accessibilityState={{ + disabled: !!(isDisabled || isExtraDay), + selected: isSelected, + }} + disabled={isDisabled || isExtraDay} + onPress={_onPress} + style={[ + theme.normalDayContainer, + isDisabled && theme.disabledDayContainer, + isSelected && theme.selectedDayContainer, + isStartOfWeek && theme.startOfWeekDayContainer, + isEndOfWeek && theme.endOfWeekDayContainer, + isStartOfMonth && theme.startOfMonthDayContainer, + isEndOfMonth && theme.endOfMonthDayContainer, + ]}> + <Text + testID={'day-text'} + style={[ + theme.normalDayText, + isDisabled && theme.disabledDayText, + isSelected && theme.selectedDayText, + isStartOfWeek && theme.startOfWeekDayText, + isEndOfWeek && theme.endOfWeekDayText, + isStartOfMonth && theme.startOfMonthDayText, + isEndOfMonth && theme.endOfMonthDayText, + ]}> + {dayjs(date).date()} + </Text> + </TouchableOpacity> + ); +}; + +export default React.memo(Day); diff --git a/src/shared/libs/CometChatCalendar/Components/Days.test.tsx b/src/shared/libs/CometChatCalendar/Components/Days.test.tsx new file mode 100644 index 0000000..22259eb --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Days.test.tsx @@ -0,0 +1,216 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; + +import type { ReactTestInstance } from 'react-test-renderer'; +import { render, fireEvent } from '@testing-library/react-native'; + +import { ThemeContext } from '../Contexts'; +import { DefaultTheme } from '../Themes'; +import type { Theme } from '../Entities'; + +import Days, { Props } from './Days'; + +dayjs.extend(localizedFormat); + +// import { DayComponentType } from '../Entities'; + +const getDisabledDays = (day: ReactTestInstance): boolean => + day.props.accessibilityState.disabled; +const getSelectedDays = (day: ReactTestInstance): boolean => + day.props.accessibilityState.selected; +const getAccessibilityLabel = (day: ReactTestInstance): string => + day.props.accessibilityLabel; + +test('Days renders without error', () => { + const days = new DaysPage({}); + expect(days.component).toBeTruthy(); +}); + +describe('Renders the correct number of normal days', () => { + test('works for months with 31 days', () => { + // January 2020 had 31 days + const days = new DaysPage({ visibleDate: dayjs('2020-01-01') }); + expect(days.normalDayArray.length).toBe(31); + }); + + test('works for months with 30 days', () => { + // April 2020 had 30 days + const days = new DaysPage({ visibleDate: dayjs('2020-04-01') }); + expect(days.normalDayArray.length).toBe(30); + }); + + test('works for leap-year Febuary', () => { + // Febuary 2020 had 29 days + const days = new DaysPage({ visibleDate: dayjs('2020-02-01') }); + expect(days.normalDayArray.length).toBe(29); + }); + + test('works for non-leap-year Febuary', () => { + // February 2019 had 28 days + const days = new DaysPage({ visibleDate: dayjs('2019-02-01') }); + expect(days.normalDayArray.length).toBe(28); + }); +}); + +describe('Renders correct number of extra days', () => { + test('works with months that need an extra row', () => { + // January 2020 needs a final padding row + const days = new DaysPage({ visibleDate: dayjs('2020-01-01') }); + expect(days.extraDayArray.length).toBe(11); + }); + + test('works with months that do not need an extra row', () => { + // May 2020 does not need a final padding row + const days = new DaysPage({ visibleDate: dayjs('2020-05-01') }); + expect(days.extraDayArray.length).toBe(11); + }); +}); + +test('Component passes onPressDay callback to Day children', () => { + const onPressDay = jest.fn(); + const days = new DaysPage({ onPressDay }); + days.randomDay && fireEvent.press(days.randomDay); + expect(onPressDay).toHaveBeenCalledTimes(1); +}); + +describe('Selects the correct days', () => { + test('none selected', () => { + const days = new DaysPage({ dateProperties: {} }); + const selectedDays = days.normalDayArray.filter(getSelectedDays); + expect(selectedDays.length).toBe(0); + }); + + test('date selection mode', () => { + const visibleDate = dayjs('2020-01-01'); + const dateProperties = { + '2019-01-01': { isSelected: true }, // same month of last year, is not rendered + '2020-01-05': { isSelected: true }, + '2020-01-15': { isSelected: true }, + '2020-01-31': { isSelected: true }, + '2020-02-01': { isSelected: true }, // next month, is not rendered + }; + + const days = new DaysPage({ visibleDate, dateProperties }); + const selectedDays = days.normalDayArray.filter(getSelectedDays); + const selectedDayLabels = selectedDays.map(getAccessibilityLabel); + + expect(selectedDays.length).toBe(3); + expect(selectedDayLabels[0]).toBe('January 5, 2020'); + expect(selectedDayLabels[1]).toBe('January 15, 2020'); + expect(selectedDayLabels[2]).toBe('January 31, 2020'); + }); +}); + +describe('Disables the correct days', () => { + test('none disabled', () => { + const days = new DaysPage({ dateProperties: {} }); + const disabledDays = days.normalDayArray.filter(getDisabledDays); + expect(disabledDays.length).toBe(0); + }); + + test('some disabled', () => { + const visibleDate = dayjs('2020-01-01'); + const dateProperties = { + '2019-01-01': { isDisabled: true }, // same month of last year, is not rendered + '2020-01-05': { isDisabled: true }, + '2020-01-15': { isDisabled: true }, + '2020-01-31': { isDisabled: true }, + '2020-02-01': { isDisabled: true }, // next month, is not rendered + }; + + const days = new DaysPage({ visibleDate, dateProperties }); + const disabledDays = days.normalDayArray.filter(getDisabledDays); + const disabledDayLabels = disabledDays.map(getAccessibilityLabel); + + expect(disabledDays.length).toBe(3); + expect(disabledDayLabels[0]).toBe('January 5, 2020'); + expect(disabledDayLabels[1]).toBe('January 15, 2020'); + expect(disabledDayLabels[2]).toBe('January 31, 2020'); + }); + + test('Disables dates before min date', () => { + const visibleDate = dayjs('2020-01-01'); + const minDate = '2020-01-04'; // 1-3 should be disabled + const days = new DaysPage({ visibleDate, minDate }); + const disabledDays = days.normalDayArray.filter(getDisabledDays); + const disabledDayLabels = disabledDays.map(getAccessibilityLabel); + + expect(disabledDays.length).toBe(3); + expect(disabledDayLabels[0]).toBe('January 1, 2020'); + expect(disabledDayLabels[1]).toBe('January 2, 2020'); + expect(disabledDayLabels[2]).toBe('January 3, 2020'); + }); + + test('Disables dates after max date', () => { + const visibleDate = dayjs('2020-01-01'); + const maxDate = '2020-01-28'; // 29-31 should be disabled + const days = new DaysPage({ visibleDate, maxDate }); + const disabledDays = days.normalDayArray.filter(getDisabledDays); + const disabledDayLabels = disabledDays.map(getAccessibilityLabel); + + expect(disabledDays.length).toBe(3); + expect(disabledDayLabels[0]).toBe('January 29, 2020'); + expect(disabledDayLabels[1]).toBe('January 30, 2020'); + expect(disabledDayLabels[2]).toBe('January 31, 2020'); + }); +}); + +describe('Theme context', () => { + test('Days container applies daysContainer theme', () => { + const days = new DaysPage({ theme: testTheme }); + expect(days.component).toHaveStyle(testTheme.daysContainer); + }); +}); + +class DaysPage { + component: ReactTestInstance; + randomDay?: ReactTestInstance; + randomCustomDay?: ReactTestInstance; + normalDayArray: ReactTestInstance[]; + extraDayArray: ReactTestInstance[]; + customDayArray: ReactTestInstance[]; + + constructor({ + visibleDate = dayjs('2020-01-01'), + showExtraDates = false, + onPressDay = () => {}, + dateProperties = {}, + theme = DefaultTheme, + minDate, + maxDate, + DayComponent, + }: Partial<Props> & { theme?: Theme }) { + const { getByTestId, queryAllByTestId } = render( + <ThemeContext.Provider value={theme}> + <Days + {...{ + minDate, + maxDate, + visibleDate, + showExtraDates, + onPressDay, + dateProperties, + DayComponent, + }} + /> + </ThemeContext.Provider> + ); + + this.component = getByTestId('days-container'); + this.normalDayArray = queryAllByTestId('day-container'); + this.extraDayArray = queryAllByTestId('extra-day-container'); + this.customDayArray = queryAllByTestId('custom-day'); + + this.randomDay = this.normalDayArray.length ? this.normalDayArray[0] : undefined; + this.randomCustomDay = this.customDayArray.length ? this.customDayArray[0] : undefined; + } +} + +const testTheme = { + ...DefaultTheme, + daysContainer: { + marginTop: 10, + backgroundColor: 'black', + }, +}; diff --git a/src/shared/libs/CometChatCalendar/Components/Days.tsx b/src/shared/libs/CometChatCalendar/Components/Days.tsx new file mode 100644 index 0000000..1b70180 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Days.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { View } from 'react-native'; +import type { Dayjs } from 'dayjs'; + +import type { DateProperties, Theme, DayComponentType } from '../Entities'; +import { useSurroundingTimeUnits } from '../Hooks'; +import { getExtraDays, dateRange } from '../Utils'; +import { ThemeContext } from '../Contexts'; +import { Day as DefaultDay } from '.'; + +export interface Props { + DayComponent?: DayComponentType; + visibleDate: Dayjs; + minDate?: string; + maxDate?: string; + showExtraDates: boolean; // Fill in empty day slots with previous and next month's days + onPressDay: (date: string) => void; // Receives date in YYYY-MM-DD format (ex. 2020-01-15 for January 15th, 2020) + dateProperties: { + [date: string /* YYYY-MM-DD */]: DateProperties; + }; +} + +const Days: React.FC<Props> = ({ + minDate, + maxDate, + visibleDate, + showExtraDates, + onPressDay, + dateProperties, + DayComponent: CustomDay, +}) => { + + const theme = React.useContext<Theme>(ThemeContext); + const { month } = useSurroundingTimeUnits(visibleDate.local().format()); + const daysOfVisibleMonth = dateRange(month.current.start, month.current.end); + + const initSlotsAvailable = month.current.start.day(); + const daysInWeek = 6; // 0-indexed + let finalSlotsAvailable = daysInWeek - month.current.end.day(); + + const nOfSlotsToFill6Rows = 7 * 6; + if ( + initSlotsAvailable + daysOfVisibleMonth.length + finalSlotsAvailable < + nOfSlotsToFill6Rows + ) { + // Add an extra row at the end of the calendar + finalSlotsAvailable += 7; + } + + const initialSlots: Dayjs[] = getExtraDays({ + from: month.last.end.subtract(initSlotsAvailable - 1, 'day'), + to: month.last.end, + }); + + const finalSlots: Dayjs[] = getExtraDays({ + from: month.next.start, + to: month.next.start.add(finalSlotsAvailable - 1, 'day'), + }); + + const Day = CustomDay || DefaultDay; + return ( + <View style={theme.daysContainer} testID={'days-container'}> + {initialSlots.map((day, index) => ( + <Day + key={index} + showExtraDates={showExtraDates} + date={day.format()} + onPress={onPressDay} + isDisabled + isExtraDay + /> + ))} + {daysOfVisibleMonth.map((day) => { + const dayProperties = dateProperties[day.format('YYYY-MM-DD')]; + return ( + <Day + {...dayProperties} + key={day.date()} + date={day.local().format('YYYY-MM-DD')} + showExtraDates={showExtraDates} + isStartOfMonth={month.current.start.isSame(day, 'day')} + isEndOfMonth={month.current.end.isSame(day, 'day')} + isStartOfWeek={day.day() === 0} + isEndOfWeek={day.day() === 6} + onPress={onPressDay} + isExtraDay={false} + isDisabled={ + dayProperties?.isDisabled || + Boolean(minDate && day.isBefore(minDate, 'day')) || + Boolean(maxDate && day.isAfter(maxDate, 'day')) + } + /> + ); + })} + {finalSlots.map((day, index) => ( + <Day + key={index} + date={day.format()} + showExtraDates={showExtraDates} + onPress={onPressDay} + isDisabled + isExtraDay + /> + ))} + </View> + ); +}; + +export default Days; diff --git a/src/shared/libs/CometChatCalendar/Components/Month.test.tsx b/src/shared/libs/CometChatCalendar/Components/Month.test.tsx new file mode 100644 index 0000000..f802e60 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Month.test.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import LocaleData from 'dayjs/plugin/localeData'; +import type { ReactTestInstance } from 'react-test-renderer'; +import { render, fireEvent } from '@testing-library/react-native'; + +import { ThemeContext } from '../Contexts'; +import { DefaultTheme } from '../Themes'; +import { DefaultLocale } from '../Locales'; + +dayjs.extend(LocaleData); + +import Month, { Props } from './Month'; + +test('Month renders without error', () => { + const month = new MonthPage({}); + expect(month.component).toBeTruthy(); +}); + +test('Component calls onPress callback when clicked', () => { + const onPress = jest.fn(); + const month = new MonthPage({ onPress }); + fireEvent.press(month.component); + expect(onPress).toHaveBeenCalledTimes(1); +}); + +describe('Enabling and disabling', () => { + test('Disable clicking if prop ´isDisabled´ is true', () => { + const month = new MonthPage({ isDisabled: true }); + expect(month.component).toBeDisabled(); + }); + + test('Enable clicking if prop ´isDisabled´ is false', () => { + const month = new MonthPage({ isDisabled: false }); + expect(month.component).toBeEnabled(); + }); +}); + +describe('Theme context', () => { + test('Container applies normal theme in enabled state', () => { + const month = new MonthPage({ isDisabled: false, isSelected: false }); + expect(month.component).toHaveStyle(theme.normalMonthContainer); + expect(month.component).not.toHaveStyle(theme.selectedMonthContainer); + expect(month.component).not.toHaveStyle(theme.disabledMonthContainer); + }); + + test('Container applies disabled theme in disabled state', () => { + const month = new MonthPage({ isDisabled: true }); + expect(month.component).toHaveStyle([ + theme.normalMonthContainer, + theme.disabledMonthContainer, + ]); + expect(month.component).not.toHaveStyle(theme.selectedMonthContainer); + }); + + test('Container applies selected theme in selected state', () => { + const month = new MonthPage({ isSelected: true }); + expect(month.component).toHaveStyle([ + theme.normalMonthContainer, + theme.selectedMonthContainer, + ]); + expect(month.component).not.toHaveStyle(theme.disabledMonthContainer); + }); + + test('Container applies selected + disabled themes in selected + disabled state', () => { + const month = new MonthPage({ isSelected: true, isDisabled: true }); + expect(month.component).toHaveStyle([ + theme.normalMonthContainer, + theme.selectedMonthContainer, + theme.disabledMonthContainer, + ]); + }); + + test('Text applies normal theme in enabled state', () => { + const month = new MonthPage({ isDisabled: false, isSelected: false }); + expect(month.text).toHaveStyle(theme.normalMonthText); + expect(month.text).not.toHaveStyle(theme.selectedMonthText); + expect(month.text).not.toHaveStyle(theme.disabledMonthText); + }); + + test('Text applies disabled theme in disabled state', () => { + const month = new MonthPage({ isDisabled: true }); + expect(month.text).toHaveStyle([theme.normalMonthText, theme.disabledMonthText]); + expect(month.text).not.toHaveStyle(theme.selectedMonthText); + }); + + test('Text applies selected theme in selected state', () => { + const month = new MonthPage({ isSelected: true }); + expect(month.text).toHaveStyle([theme.normalMonthText, theme.selectedMonthText]); + expect(month.text).not.toHaveStyle(theme.disabledMonthText); + }); + + test('Text applies selected + disabled themes in selected + disabled state', () => { + const month = new MonthPage({ isSelected: true, isDisabled: true }); + expect(month.text).toHaveStyle([ + theme.normalMonthText, + theme.selectedMonthText, + theme.disabledMonthText, + ]); + }); +}); + +class MonthPage { + component: ReactTestInstance; + text: ReactTestInstance; + + constructor({ + date = '2020-01-01', + onPress = () => {}, + locale = DefaultLocale, + isSelected = false, + isDisabled = false, + }: Partial<Props>) { + const { getByLabelText, getByTestId } = render( + <ThemeContext.Provider value={theme}> + <Month {...{ locale, date, onPress, isSelected, isDisabled }} /> + </ThemeContext.Provider> + ); + + const months = dayjs(date).localeData().monthsShort(); + const index = dayjs(date).month(); + + this.component = getByLabelText(months[index]); + this.text = getByTestId('month-text'); + } +} + +// Mix of properties which get overwritten and which don't +const theme = { + ...DefaultTheme, + normalMonthContainer: { + marginTop: 10, + backgroundColor: 'black', + }, + selectedMonthContainer: { + marginBottom: 10, + backgroundColor: 'green', + }, + disabledMonthContainer: { + marginLeft: 10, + backgroundColor: 'gray', + }, + normalMonthText: { + marginTop: 10, + color: 'black', + }, + selectedMonthText: { + marginBottom: 10, + color: 'green', + }, + disabledMonthText: { + marginLeft: 10, + color: 'gray', + }, +}; + +export const testLocale = { + ...DefaultLocale, + monthsShort: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'], +}; diff --git a/src/shared/libs/CometChatCalendar/Components/Month.tsx b/src/shared/libs/CometChatCalendar/Components/Month.tsx new file mode 100644 index 0000000..b3a3edd --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Month.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import { Text, TouchableOpacity } from 'react-native'; +import { ThemeContext } from '../Contexts'; +import type { Theme, Locale } from '../Entities'; + +export interface Props { + date: string; + locale: Locale; + onPress: (date: string) => void; + isSelected: boolean; + isDisabled: boolean; +} + +const Month: React.FC<Props> = ({ date, onPress, isSelected, isDisabled, locale }) => { + const theme = React.useContext<Theme>(ThemeContext); + const _onPress = React.useCallback(() => onPress(date), [date, onPress]); + + return ( + <TouchableOpacity + testID={'month-container'} + accessibilityRole={'button'} + accessibilityLabel={dayjs(date).locale(locale).format('MMM')} + accessibilityState={{ disabled: isDisabled, selected: isSelected }} + accessibilityHint={'Press to select this month and return to calendar month view'} + disabled={isDisabled} + onPress={_onPress} + style={[ + theme.normalMonthContainer, + isSelected && theme.selectedMonthContainer, + isDisabled && theme.disabledMonthContainer, + ]}> + <Text + testID={'month-text'} + style={[ + theme.normalMonthText, + isSelected && theme.selectedMonthText, + isDisabled && theme.disabledMonthText, + ]}> + {dayjs(date).locale(locale).format('MMM')} + </Text> + </TouchableOpacity> + ); +}; + +export default React.memo(Month); diff --git a/src/shared/libs/CometChatCalendar/Components/Months.test.tsx b/src/shared/libs/CometChatCalendar/Components/Months.test.tsx new file mode 100644 index 0000000..9f15870 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Months.test.tsx @@ -0,0 +1,262 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import { TouchableOpacity, Text } from 'react-native'; +import { fireEvent, render } from '@testing-library/react-native'; +import type { ReactTestInstance } from 'react-test-renderer'; + +import { ThemeContext } from '../Contexts'; +import { DefaultTheme } from '../Themes'; +import type { Theme, MonthComponentType } from '../Entities'; + +import Months, { Props } from './Months'; + +const getDisabledMonths = (month: ReactTestInstance) => + month.props.accessibilityState.disabled; +const getSelectedMonths = (month: ReactTestInstance) => + month.props.accessibilityState.selected; + +test('Months renders without error', () => { + const months = new MonthsPage({}); + expect(months.component).toBeTruthy(); +}); + +test('Renders the correct number of months', () => { + const months = new MonthsPage({}); + expect(months.monthArray.length).toBe(12); +}); + +test('Component passes onPressMonth callback to Month children', () => { + const onPressMonth = jest.fn(); + const months = new MonthsPage({ onPressMonth }); + months.firstMonth && fireEvent.press(months.firstMonth); + expect(onPressMonth).toHaveBeenCalledTimes(1); +}); + +describe('Generates correct set of selected months', () => { + test('None selected, undefined', () => { + const months = new MonthsPage({ dateProperties: {} }); + const selectedMonths = months.monthArray.filter(getSelectedMonths); + expect(selectedMonths.length).toBe(0); + }); + + test('None selected, false', () => { + const months = new MonthsPage({ + dateProperties: { + '2020-04-01': { isSelected: false }, + }, + }); + const selectedMonths = months.monthArray.filter(getSelectedMonths); + expect(selectedMonths.length).toBe(0); + }); + + test('Date selection', () => { + const dateProperties = { + '2019-01-01': { isSelected: true }, + '2020-01-01': { isSelected: true }, + '2020-01-05': { isSelected: true }, + '2020-04-23': { isSelected: true }, + }; + + const months = new MonthsPage({ dateProperties }); + const selectedMonths = months.monthArray.filter(getSelectedMonths); + expect(selectedMonths.length).toBe(2); + expect(selectedMonths[0]).toHaveTextContent('Jan'); + expect(selectedMonths[1]).toHaveTextContent('Apr'); + }); +}); + +describe('Disables the correct months', () => { + describe('due to min date', () => { + test('works for start-of-year date', () => { + const months = new MonthsPage({ minDate: '2020-01-01' }); + const disabledMonths = months.monthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(0); + }); + + test('works for middle-of-the-year dates', () => { + const months = new MonthsPage({ minDate: '2020-04-01' }); + const disabledMonths = months.monthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(3); + expect(disabledMonths[0]).toHaveTextContent('Jan'); + expect(disabledMonths[1]).toHaveTextContent('Feb'); + expect(disabledMonths[2]).toHaveTextContent('Mar'); + }); + + test('works for end-of-year date', () => { + const months = new MonthsPage({ minDate: '2020-12-31' }); + const disabledMonths = months.monthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(11); + }); + }); + + describe('due to max date', () => { + test('works for start-of-year date', () => { + const months = new MonthsPage({ maxDate: '2020-01-01' }); + const disabledMonths = months.monthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(11); + }); + + test('works for middle-of-the-year date', () => { + const months = new MonthsPage({ maxDate: '2020-10-15' }); + const disabledMonths = months.monthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(2); + expect(disabledMonths[0]).toHaveTextContent('Nov'); + expect(disabledMonths[1]).toHaveTextContent('Dec'); + }); + + test('works for end-of-year date', () => { + const months = new MonthsPage({ maxDate: '2020-12-31' }); + const disabledMonths = months.monthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(0); + }); + }); +}); + +describe('Theme context', () => { + test('Months container applies monthsContainer theme', () => { + const months = new MonthsPage({ theme: testTheme }); + expect(months.component).toHaveStyle(testTheme.monthsContainer); + }); +}); + +describe('Custom month component', () => { + const MonthComponent: MonthComponentType = ({ + date, + onPress, + isSelected, + isDisabled, + }) => ( + <TouchableOpacity + testID={'custom-month'} + accessibilityState={{ disabled: isDisabled, selected: isSelected }} + onPress={() => onPress(date)}> + <Text>{dayjs(date).format('MMM')}</Text> + </TouchableOpacity> + ); + + test('custom month receives onPress prop', () => { + const onPressMonth = jest.fn(); + const months = new MonthsPage({ MonthComponent, onPressMonth }); + months.firstCustomMonth && fireEvent.press(months.firstCustomMonth); + expect(onPressMonth).toHaveBeenCalledTimes(1); + }); + + describe('custom month receives isSelected prop', () => { + test('None selected', () => { + const months = new MonthsPage({ MonthComponent, dateProperties: {} }); + const selectedMonths = months.customMonthArray.filter(getSelectedMonths); + expect(selectedMonths.length).toBe(0); + }); + + test('Date selection', () => { + const dateProperties = { + '2019-01-01': { isSelected: true }, + '2020-01-01': { isSelected: true }, + '2020-01-05': { isSelected: true }, + '2020-04-23': { isSelected: true }, + }; + + const months = new MonthsPage({ MonthComponent, dateProperties }); + const selectedMonths = months.customMonthArray.filter(getSelectedMonths); + expect(selectedMonths.length).toBe(2); + }); + }); + + describe('custom month component receives isDisabled prop', () => { + describe('disable months due to min date', () => { + test('works for start-of-year date', () => { + const minDate = '2020-01-01'; + const months = new MonthsPage({ MonthComponent, minDate }); + const disabledMonths = months.customMonthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(0); + }); + + test('works for middle-of-the-year dates', () => { + const minDate = '2020-04-01'; + const months = new MonthsPage({ MonthComponent, minDate }); + const disabledMonths = months.customMonthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(3); + }); + + test('works for end-of-year date', () => { + const minDate = '2020-12-31'; + const months = new MonthsPage({ MonthComponent, minDate }); + const disabledMonths = months.customMonthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(11); + }); + }); + + describe('disable months due to max date', () => { + test('works for start-of-year date', () => { + const maxDate = '2020-01-01'; + const months = new MonthsPage({ MonthComponent, maxDate }); + const disabledMonths = months.customMonthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(11); + }); + + test('works for middle-of-the-year dates', () => { + const maxDate = '2020-10-15'; + const months = new MonthsPage({ MonthComponent, maxDate }); + const disabledMonths = months.customMonthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(2); + }); + + test('works for end-of-year date', () => { + const maxDate = '2020-12-31'; + const months = new MonthsPage({ MonthComponent, maxDate }); + const disabledMonths = months.customMonthArray.filter(getDisabledMonths); + expect(disabledMonths.length).toBe(0); + }); + }); + }); +}); + +export class MonthsPage { + component: ReactTestInstance; + firstMonth?: ReactTestInstance; + firstCustomMonth?: ReactTestInstance; + monthArray: ReactTestInstance[]; + customMonthArray: ReactTestInstance[]; + + constructor({ + onPressMonth = () => {}, + dateProperties = {}, + visibleDate = dayjs('2020-05-01'), + MonthComponent, + minDate, + maxDate, + theme = DefaultTheme, + }: Partial<Props> & { theme?: Theme }) { + const { getAllByRole, getByTestId, getAllByTestId } = render( + <ThemeContext.Provider value={theme}> + <Months + {...{ + MonthComponent, + onPressMonth, + dateProperties, + visibleDate, + minDate, + maxDate, + }} + /> + </ThemeContext.Provider> + ); + + this.component = getByTestId('months-container'); + this.monthArray = !MonthComponent ? getAllByRole('button') : []; + this.customMonthArray = MonthComponent ? getAllByTestId('custom-month') : []; + + this.firstMonth = this.monthArray.length ? this.monthArray[0] : undefined; + this.firstCustomMonth = this.customMonthArray.length + ? this.customMonthArray[0] + : undefined; + } +} + +export const testTheme = { + ...DefaultTheme, + monthsContainer: { + marginTop: 10, + backgroundColor: 'black', + }, +}; diff --git a/src/shared/libs/CometChatCalendar/Components/Months.tsx b/src/shared/libs/CometChatCalendar/Components/Months.tsx new file mode 100644 index 0000000..0cbe75f --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Months.tsx @@ -0,0 +1,78 @@ +import React, { useMemo } from 'react'; +import { View } from 'react-native'; +import dayjs, { Dayjs } from 'dayjs'; + +import type { DateProperties, Theme, MonthComponentType, Locale } from '../Entities'; +import { ThemeContext, LocaleContext } from '../Contexts'; +import { dateRange } from '../Utils'; + +import { Month as DefaultMonth } from '.'; + +export interface Props { + MonthComponent?: MonthComponentType; + // Receives a start-of-month date in YYYY-MM-DD format (ex. 2020-01-01 for the month of January, 2020) + onPressMonth: (date: string) => void; + // YYYY-MM-DD format string respresenting the start-of-month date of currently visible month + visibleDate: Dayjs; + // YYYY-MM-DD format string respresenting the minimum date that can be selected + minDate?: string; + // YYYY-MM-DD format string respresenting the maximum date that can be selected + maxDate?: string; + dateProperties: { + [date: string /* YYYY-MM-DD */]: DateProperties; + }; +} + +const Months: React.FC<Props> = ({ + MonthComponent: CustomMonth, + onPressMonth, + dateProperties, + visibleDate, + minDate, + maxDate, +}) => { + const theme = React.useContext<Theme>(ThemeContext); + const locale = React.useContext<Locale>(LocaleContext); + + const monthsOfVisibleYear = dateRange( + visibleDate.startOf('year'), + visibleDate.endOf('year'), + 'month' + ); + + const selectedMonths = useMemo(() => { + return Object.entries(dateProperties).reduce( + (selected: Record<string, boolean>, [date, properties]) => { + if (properties.isSelected) { + const month = dayjs(date).startOf('month').format('YYYY-MM-DD'); + selected[month] = true; + } + return selected; + }, + {} + ); + }, [dateProperties]); + + const Month = CustomMonth || DefaultMonth; + return ( + <View style={theme.monthsContainer} testID={'months-container'}> + {monthsOfVisibleYear.map((month, index) => { + return ( + <Month + key={index} + locale={locale} + date={month.format('YYYY-MM-DD')} + isSelected={selectedMonths[month.format('YYYY-MM-DD')]} + onPress={onPressMonth} + isDisabled={ + !!(maxDate && month.startOf('month').isAfter(maxDate, 'day')) || + !!(minDate && month.endOf('month').isBefore(minDate, 'day')) + } + /> + ); + })} + </View> + ); +}; + +export default React.memo(Months); diff --git a/src/shared/libs/CometChatCalendar/Components/Title.test.tsx b/src/shared/libs/CometChatCalendar/Components/Title.test.tsx new file mode 100644 index 0000000..dad4749 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Title.test.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +import type { ReactTestInstance } from 'react-test-renderer'; +import { render, fireEvent } from '@testing-library/react-native'; + +import { ThemeContext } from '../Contexts'; +import { DefaultTheme } from '../Themes'; +import { DefaultLocale } from '../Locales'; +import { VIEW } from '../Constants'; + +import Title, { Props } from './Title'; + +dayjs.extend(utc); + +test('Title render without error', () => { + const title = new TitlePage({}); + expect(title.component).toBeTruthy(); +}); + +test('Title renders date string in default format', () => { + const title = new TitlePage({ date: '2020-09-18', locale: DefaultLocale }); + expect(title.text).toHaveTextContent('September 2020'); +}); + +describe('Title format switches depending on the active view', () => { + test('Month view format', () => { + const title = new TitlePage({ date: '2020-09-18', activeView: VIEW.MONTH }); + expect(title.text).toHaveTextContent('September 2020'); + }); + + test('Year view format', () => { + const title = new TitlePage({ date: '2020-09-18', activeView: VIEW.YEAR }); + expect(title.text).toHaveTextContent('2020'); + }); +}); + +test('Title receives locale context', () => { + const title = new TitlePage({ date: '2020-09-18', locale: testLocale }); + expect(title.text).toHaveTextContent('I 2020'); +}); + +test('Title calls onPress callback when clicked', () => { + const onPress = jest.fn(); + const title = new TitlePage({ onPress }); + fireEvent.press(title.component); + expect(onPress).toHaveBeenCalledTimes(1); +}); + +describe('Enabling and disabling', () => { + test('Disable clicking if prop ´isDisabled´ is true', () => { + const month = new TitlePage({ isDisabled: true }); + expect(month.component).toBeDisabled(); + }); + + test('Enable clicking if prop ´isDisabled´ is false', () => { + const month = new TitlePage({ isDisabled: false }); + expect(month.component).toBeEnabled(); + }); +}); + +describe('Theme context', () => { + test('Title container applies titleContainer theme', () => { + const title = new TitlePage({}); + expect(title.component).toHaveStyle(theme.titleContainer); + }); + + test('Title text applies titleText theme', () => { + const title = new TitlePage({}); + expect(title.text).toHaveStyle(theme.titleText); + }); +}); + +class TitlePage { + component: ReactTestInstance; + text: ReactTestInstance; + + constructor({ + date = '2020-01-01', + locale = DefaultLocale, + onPress = () => {}, + isDisabled = false, + activeView = VIEW.MONTH, + }: Partial<Props>) { + const { getByTestId, getByRole } = render( + <ThemeContext.Provider value={theme}> + <Title {...{ locale, activeView, date, onPress, isDisabled }} /> + </ThemeContext.Provider> + ); + + this.component = getByRole('button'); + this.text = getByTestId('title-text'); + } +} + +const testLocale = { + ...DefaultLocale, + months: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'], +}; + +const theme = { + ...DefaultTheme, + titleContainer: { + marginTop: 10, + backgroundColor: 'orange', + }, + titleText: { + color: 'pink', + }, +}; diff --git a/src/shared/libs/CometChatCalendar/Components/Title.tsx b/src/shared/libs/CometChatCalendar/Components/Title.tsx new file mode 100644 index 0000000..ae17435 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Title.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +import { Text, TouchableOpacity } from 'react-native'; +import { VIEW } from '../Constants'; +import type { Theme, Locale } from '../Entities'; +import { ThemeContext } from '../Contexts'; + +dayjs.extend(utc); + +export interface Props { + date: string; + locale: Locale; + onPress: (date: string) => void; + isDisabled?: boolean; + activeView: VIEW; +} + +const Title: React.FC<Props> = ({ activeView, date, onPress, isDisabled, locale }) => { + const theme: Theme = React.useContext<Theme>(ThemeContext); + const _date = dayjs(date).locale(locale).local(); + + const title = + activeView === VIEW.MONTH ? _date.format('MMMM YYYY') : _date.format('YYYY'); + const _onPress = React.useCallback(() => onPress(date), [date, onPress]); + + return ( + <TouchableOpacity + testID={'title'} + accessibilityRole={'button'} + accessibilityHint={'Press to switch calendar views'} + accessibilityLabel={title} + accessibilityState={{ disabled: isDisabled }} + style={theme.titleContainer} + disabled={isDisabled} + onPress={_onPress}> + <Text testID={'title-text'} style={theme.titleText}> + {title} + </Text> + </TouchableOpacity> + ); +}; + +export default Title; diff --git a/src/shared/libs/CometChatCalendar/Components/Weekdays.test.tsx b/src/shared/libs/CometChatCalendar/Components/Weekdays.test.tsx new file mode 100644 index 0000000..02a190d --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Weekdays.test.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import type { ReactTestInstance } from 'react-test-renderer'; +import { render } from '@testing-library/react-native'; +import { ThemeContext, LocaleContext } from '../Contexts'; +import { DefaultTheme } from '../Themes'; +import { DefaultLocale } from '../Locales'; + +import Weekdays from './Weekdays'; + +test('Weekdays render without error', () => { + const weekdays = new WeekdaysPage(); + expect(weekdays.component).toBeTruthy(); +}); + +test('Weekday receives locale context', () => { + const weekdays = new WeekdaysPage(); + expect(weekdays.array).toStrictEqual(locale.weekdays); +}); + +describe('Theme context', () => { + test('Weekday container applies weekdayContainer theme', () => { + const weekdays = new WeekdaysPage(); + expect(weekdays.component).toHaveStyle(theme.weekdaysContainer); + }); + + test('Weekday text applies weekdayText theme', () => { + const weekdays = new WeekdaysPage(); + expect(weekdays.text).toHaveStyle(theme.weekdayText); + }); +}); + +class WeekdaysPage { + component: ReactTestInstance; + text: ReactTestInstance; + array: (string | ReactTestInstance)[]; + + constructor() { + const { getByTestId, getAllByTestId } = render( + <LocaleContext.Provider value={locale}> + <ThemeContext.Provider value={theme}> + <Weekdays days={locale.weekdays} /> + </ThemeContext.Provider> + </LocaleContext.Provider> + ); + + const allWeekdays = getAllByTestId('weekday'); + + this.component = getByTestId('weekdays'); + this.array = allWeekdays.map((day: ReactTestInstance) => day.children[0]); + this.text = allWeekdays[0]; + } +} + +const locale = { + ...DefaultLocale, + weekdays: ['A', 'B', 'C', 'D', 'E', 'F', 'G'], +}; + +const theme = { + ...DefaultTheme, + weekdaysContainer: { + marginTop: 10, + backgroundColor: 'black', + }, + weekdayText: { + marginBottom: 10, + color: 'green', + }, +}; diff --git a/src/shared/libs/CometChatCalendar/Components/Weekdays.tsx b/src/shared/libs/CometChatCalendar/Components/Weekdays.tsx new file mode 100644 index 0000000..9528868 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/Weekdays.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { View, Text } from 'react-native'; + +import type { Theme } from '../Entities'; +import { ThemeContext } from '../Contexts'; + +export interface Props { + days: string[]; +} + +const Weekdays: React.FC<Props> = ({ days }) => { + const theme = React.useContext<Theme>(ThemeContext); + + return ( + <View style={theme.weekdaysContainer} testID={'weekdays'}> + {days.map((day) => ( + <Text + testID={'weekday'} + accessibilityLabel={day} + key={day} + style={theme.weekdayText}> + {day} + </Text> + ))} + </View> + ); +}; + +export default React.memo(Weekdays); diff --git a/src/shared/libs/CometChatCalendar/Components/index.ts b/src/shared/libs/CometChatCalendar/Components/index.ts new file mode 100644 index 0000000..4b37074 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Components/index.ts @@ -0,0 +1,7 @@ +export { default as Arrow } from './Arrow'; +export { default as Weekdays } from './Weekdays'; +export { default as Month } from './Month'; +export { default as Months } from './Months'; +export { default as Title } from './Title'; +export { default as Day } from './Day'; +export { default as Days } from './Days'; diff --git a/src/shared/libs/CometChatCalendar/Constants/View.ts b/src/shared/libs/CometChatCalendar/Constants/View.ts new file mode 100644 index 0000000..7ef484f --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Constants/View.ts @@ -0,0 +1,4 @@ +export enum VIEW { + YEAR, + MONTH, +} diff --git a/src/shared/libs/CometChatCalendar/Constants/index.ts b/src/shared/libs/CometChatCalendar/Constants/index.ts new file mode 100644 index 0000000..450cf3c --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Constants/index.ts @@ -0,0 +1 @@ +export * from './View'; diff --git a/src/shared/libs/CometChatCalendar/Contexts/LocaleContext.tsx b/src/shared/libs/CometChatCalendar/Contexts/LocaleContext.tsx new file mode 100644 index 0000000..ab6785d --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Contexts/LocaleContext.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import type { Locale } from '../Entities'; +import { DefaultLocale } from '../Locales'; + +const LocaleContext = React.createContext<Locale>(DefaultLocale); + +export default LocaleContext; diff --git a/src/shared/libs/CometChatCalendar/Contexts/ThemeContext.tsx b/src/shared/libs/CometChatCalendar/Contexts/ThemeContext.tsx new file mode 100644 index 0000000..a2a60a0 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Contexts/ThemeContext.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import type { Theme } from '../Entities'; +import { DefaultTheme } from '../Themes'; + +const ThemeContext = React.createContext<Theme>(DefaultTheme); + +export default ThemeContext; diff --git a/src/shared/libs/CometChatCalendar/Contexts/index.ts b/src/shared/libs/CometChatCalendar/Contexts/index.ts new file mode 100644 index 0000000..729f89d --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Contexts/index.ts @@ -0,0 +1,2 @@ +export { default as ThemeContext } from './ThemeContext'; +export { default as LocaleContext } from './LocaleContext'; diff --git a/src/shared/libs/CometChatCalendar/Entities/ArrowDirections.ts b/src/shared/libs/CometChatCalendar/Entities/ArrowDirections.ts new file mode 100644 index 0000000..57eb47a --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Entities/ArrowDirections.ts @@ -0,0 +1 @@ +export type ArrowDirections = 'left' | 'right'; diff --git a/src/shared/libs/CometChatCalendar/Entities/Calendar.ts b/src/shared/libs/CometChatCalendar/Entities/Calendar.ts new file mode 100644 index 0000000..1d8319c --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Entities/Calendar.ts @@ -0,0 +1,26 @@ +import type { Locale } from './Locale'; +import type { Theme } from './Theme'; +import type { + ArrowComponentType, + MonthComponentType, + TitleComponentType, + DayComponentType, + WeekdaysComponentType, +} from './Components'; + +export interface WrapperCalendarProps { + ArrowComponent?: ArrowComponentType; + TitleComponent?: TitleComponentType; + DayComponent?: DayComponentType; + MonthComponent?: MonthComponentType; + WeekdaysComponent?: WeekdaysComponentType; + initVisibleDate?: string; + disabledDates?: string[]; + minDate?: string; + maxDate?: string; + allowYearView?: boolean; + showExtraDates?: boolean; + testID?: string; + locale?: Locale; + theme?: Theme; +} diff --git a/src/shared/libs/CometChatCalendar/Entities/Components.ts b/src/shared/libs/CometChatCalendar/Entities/Components.ts new file mode 100644 index 0000000..5bd097a --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Entities/Components.ts @@ -0,0 +1,13 @@ +import type { Props as ArrowProps } from '../Components/Arrow'; +import type { Props as TitleProps } from '../Components/Title'; +import type { Props as MonthProps } from '../Components/Month'; +import type { Props as DayProps } from '../Components/Day'; +import type { Props as WeekdaysProps } from '../Components/Weekdays'; + +export type ArrowComponentType = (props: ArrowProps) => JSX.Element | null; +export type TitleComponentType = (props: TitleProps) => JSX.Element | null; +export type MonthComponentType = (props: MonthProps) => JSX.Element | null; +export type DayComponentType = (props: DayProps) => JSX.Element | null; +export type WeekdaysComponentType = (props: WeekdaysProps) => JSX.Element | null; + +export type { ArrowProps, TitleProps, MonthProps, DayProps, WeekdaysProps }; diff --git a/src/shared/libs/CometChatCalendar/Entities/DateProperties.ts b/src/shared/libs/CometChatCalendar/Entities/DateProperties.ts new file mode 100644 index 0000000..f29ba46 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Entities/DateProperties.ts @@ -0,0 +1,6 @@ +export interface DateProperties { + // Used in date selection calendar + isSelected?: boolean; + // Used in both calendars + isDisabled?: boolean; +} diff --git a/src/shared/libs/CometChatCalendar/Entities/Locale.ts b/src/shared/libs/CometChatCalendar/Entities/Locale.ts new file mode 100644 index 0000000..923363b --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Entities/Locale.ts @@ -0,0 +1,12 @@ +import type { DefaultLocale } from '../Locales'; + +export type Locale = typeof DefaultLocale; + +export interface LocaleData { + // firstDayOfWeek: Function; + // longDateFormat: Function; + months: () => string[]; + monthsShort: () => string[]; + weekdaysMin: () => string[]; + weekdaysShort: () => string[]; +} diff --git a/src/shared/libs/CometChatCalendar/Entities/Theme.ts b/src/shared/libs/CometChatCalendar/Entities/Theme.ts new file mode 100644 index 0000000..dc86a06 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Entities/Theme.ts @@ -0,0 +1,70 @@ +import type { StyleProp, TextStyle, ViewStyle, ImageStyle } from 'react-native'; + +/* + * All "container" styles are currently implemented as View or TouchableOpacity components + * All other styles are appended with their component type. + * Ex: arrowImage is an Image component, titleText is a Text component + */ + +export interface Theme { + calendarContainer: StyleProp<ViewStyle>; // Outermost container + headerContainer: StyleProp<ViewStyle>; // Wraps arrows and title + /* + * ------- ARROW STYLES ---------- + * Different arrow states have different styles: + * 1. normal arrow // applies to all calendars, all views + * 2. disabled arrow // applies to all calendars, all views + */ + normalArrowContainer: StyleProp<ViewStyle>; + disabledArrowContainer: StyleProp<ViewStyle>; + normalArrowImage: StyleProp<ImageStyle>; + disabledArrowImage: StyleProp<ImageStyle>; + + titleContainer: StyleProp<ViewStyle>; + titleText: StyleProp<TextStyle>; + weekdaysContainer: StyleProp<ViewStyle>; // Wraps all the weekday names + weekdayText: StyleProp<TextStyle>; + daysContainer: StyleProp<ViewStyle>; // Wraps all the days of the month in MonthView + monthsContainer: StyleProp<ViewStyle>; // Wraps all the months of the year in YearView + /* + * ------- MONTH STYLES ---------- + * Different month states have different styles: + * 1. normal month // applies to all calendars, year view + * 2. disabled month // applies to all calendars, year view + * 3. selected month // applies to all calendars, year view + */ + normalMonthContainer: StyleProp<ViewStyle>; + disabledMonthContainer: StyleProp<ViewStyle>; + selectedMonthContainer: StyleProp<ViewStyle>; + normalMonthText: StyleProp<TextStyle>; + disabledMonthText: StyleProp<TextStyle>; + selectedMonthText: StyleProp<TextStyle>; + /* + * ------- DAY STYLES ---------- + * Different day states have different styles: + * 1. normal day // applies to all calendars, month view + * 2. disabled day // applies to all calendars, month view + * 3. start of week day // applies to all calendars, month view + * 4. end of week day // applies to all calendars, month view + * 5. start of month day // applies to all calendars, month view + * 6. end of month day // applies to all calendats, month view + * 7. selected day // applies to date selection calendar, month view + */ + normalDayContainer: StyleProp<ViewStyle>; + disabledDayContainer: StyleProp<ViewStyle>; + selectedDayContainer: StyleProp<ViewStyle>; + extraDayContainer: StyleProp<ViewStyle>; + startOfWeekDayContainer: StyleProp<ViewStyle>; + endOfWeekDayContainer: StyleProp<ViewStyle>; + startOfMonthDayContainer: StyleProp<ViewStyle>; + endOfMonthDayContainer: StyleProp<ViewStyle>; + + normalDayText: StyleProp<TextStyle>; + disabledDayText: StyleProp<TextStyle>; + selectedDayText: StyleProp<TextStyle>; + extraDayText: StyleProp<TextStyle>; + startOfWeekDayText: StyleProp<TextStyle>; + endOfWeekDayText: StyleProp<TextStyle>; + startOfMonthDayText: StyleProp<TextStyle>; + endOfMonthDayText: StyleProp<TextStyle>; +} diff --git a/src/shared/libs/CometChatCalendar/Entities/index.ts b/src/shared/libs/CometChatCalendar/Entities/index.ts new file mode 100644 index 0000000..aee3a1f --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Entities/index.ts @@ -0,0 +1,6 @@ +export * from './ArrowDirections'; +export * from './DateProperties'; +export * from './Theme'; +export * from './Locale'; +export * from './Components'; +export * from './Calendar'; diff --git a/src/shared/libs/CometChatCalendar/Hooks/index.ts b/src/shared/libs/CometChatCalendar/Hooks/index.ts new file mode 100644 index 0000000..8209057 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Hooks/index.ts @@ -0,0 +1 @@ +export { default as useSurroundingTimeUnits } from './useSurroundingTimeUnits'; diff --git a/src/shared/libs/CometChatCalendar/Hooks/useSurroundingTimeUnits.test.ts b/src/shared/libs/CometChatCalendar/Hooks/useSurroundingTimeUnits.test.ts new file mode 100644 index 0000000..92d9767 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Hooks/useSurroundingTimeUnits.test.ts @@ -0,0 +1,44 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useSurroundingTimeUnits from './useSurroundingTimeUnits'; + +test('Should return surrounding time units', () => { + const { result, rerender } = renderHook( + ({ visibleDate }) => useSurroundingTimeUnits(visibleDate), + { + initialProps: { + visibleDate: '2020-01-06', + }, + } + ); + + expect(result.current.month.current.start.isSame('2020-01-01', 'day')).toBe(true); + expect(result.current.month.current.end.isSame('2020-01-31', 'day')).toBe(true); + expect(result.current.month.last.start.isSame('2019-12-01', 'day')).toBe(true); + expect(result.current.month.last.end.isSame('2019-12-31', 'day')).toBe(true); + expect(result.current.month.next.start.isSame('2020-02-01', 'day')).toBe(true); + expect(result.current.month.next.end.isSame('2020-02-29', 'day')).toBe(true); + + rerender({ + visibleDate: '2020-01-01', + }); + + expect(result.current.month.current.start.isSame('2020-01-01', 'day')).toBe(true); + expect(result.current.month.current.end.isSame('2020-01-31', 'day')).toBe(true); + expect(result.current.month.last.start.isSame('2019-12-01', 'day')).toBe(true); + expect(result.current.month.last.end.isSame('2019-12-31', 'day')).toBe(true); + expect(result.current.month.next.start.isSame('2020-02-01', 'day')).toBe(true); + expect(result.current.month.next.end.isSame('2020-02-29', 'day')).toBe(true); + + rerender({ + visibleDate: '2020-02-01', + }); + + expect(result.current.month.current.start.isSame('2020-02-01', 'day')).toBe(true); + expect(result.current.month.current.end.isSame('2020-02-29', 'day')).toBe(true); + expect(result.current.month.last.start.isSame('2020-01-01', 'day')).toBe(true); + expect(result.current.month.last.end.isSame('2020-01-31', 'day')).toBe(true); + expect(result.current.month.next.start.isSame('2020-03-01', 'day')).toBe(true); + + // console.log(result.current.month.next.end.format()); + expect(result.current.month.next.end.isSame('2020-03-31', 'day')).toBe(true); +}); diff --git a/src/shared/libs/CometChatCalendar/Hooks/useSurroundingTimeUnits.ts b/src/shared/libs/CometChatCalendar/Hooks/useSurroundingTimeUnits.ts new file mode 100644 index 0000000..4c2538c --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Hooks/useSurroundingTimeUnits.ts @@ -0,0 +1,60 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import localeData from 'dayjs/plugin/localeData'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(localeData); +dayjs.extend(utc); + +const useSurroundingTimeUnits = (visibleDate: string) => { + return React.useMemo(() => { + const startOfMonth = dayjs(visibleDate).local().startOf('month'); + const endOfMonth = dayjs(visibleDate).local().endOf('month'); + + const month = { + current: { + start: startOfMonth, + end: endOfMonth, + }, + next: { + start: startOfMonth.add(1, 'month').startOf('month'), + end: endOfMonth.add(1, 'month').endOf('month'), + }, + last: { + start: startOfMonth.subtract(1, 'month').startOf('month'), + end: endOfMonth.subtract(1, 'month').endOf('month'), + }, + afterNext: startOfMonth.add(2, 'month'), + beforeLast: endOfMonth.subtract(2, 'month'), + }; + + const startOfYear = dayjs(visibleDate).local().startOf('year'); + const endOfYear = dayjs(visibleDate).local().endOf('year'); + + const year = { + current: { + start: startOfYear, + end: endOfYear, + }, + next: { + start: startOfYear.add(1, 'year').startOf('year'), + persistMonth: dayjs(visibleDate).local().add(1, 'year'), + end: endOfYear.add(1, 'year').endOf('year'), + }, + last: { + start: startOfYear.subtract(1, 'year').startOf('year'), + persistMonth: dayjs(visibleDate).local().subtract(1, 'year'), + end: endOfYear.subtract(1, 'year').endOf('year'), + }, + afterNext: startOfYear.add(2, 'year'), + beforeLast: endOfYear.subtract(2, 'year'), + }; + + return { + year, + month, + }; + }, [visibleDate]); +}; + +export default useSurroundingTimeUnits; diff --git a/src/shared/libs/CometChatCalendar/Icons/BackArrow.png b/src/shared/libs/CometChatCalendar/Icons/BackArrow.png new file mode 100644 index 0000000000000000000000000000000000000000..e09440a84275dec623656c2c451f37f4cf348030 GIT binary patch literal 391 zcmeAS@N?(olHy`uVBq!ia0vp^Q6S901|%(3I5Gh#&H|6fVg?3oVGw3ym^DX&fq~J| z)5S5QV$R!J8@-qVMO-fmcUyD)+b*V*nfr&M`+<BU=OU+5Cmgi6TNmViITvLRtI&V; zq3Tf;pm7X;%!4P*Rypr^Z}J`0oe{qRWR>4flCiTb&Z)Ef^k>h%E7RB9{o?xMXJzEC zhU|_SkD}dQ6t4?Zmj97aU!}r--=)r{sP2`xz(n^ADHE$T$~T?Roub-($0?F9<It0* z(+t<`bu>9P*}S7A@9C+lhR2>9FPQU5KI+Z1Q=5+Ft<;GomT_FLT0#HipN%(ePh^iO zezRR=_EP?xZ8v_aq<hO3@NRrR-7ez#joXh)H`=@Hi}@8WH}}qIpw}myvtwv+68XI; SY1u|lP<XofxvX<aXaWGC->-@Q literal 0 HcmV?d00001 diff --git a/src/shared/libs/CometChatCalendar/Icons/chevron-left-16.png b/src/shared/libs/CometChatCalendar/Icons/chevron-left-16.png new file mode 100644 index 0000000000000000000000000000000000000000..149e9f96fc74960fe0bd59d68d33e817351610c9 GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_Ao-U3d z7QK5fJ90HSh%`K$uQ&1TG2ssHW`~nkQe8?e1?=gX<TsnG?YP2cw-;+`y5EPV3H&n= z{lGR!qJ~xM0E2SDr3beTKKnlB?ZHrueU$~0UDA#AQg;|vADFINAh7%6o8T9>-<r%@ zZ;`FMRy`;~xm<8&V~ljA1!J|`O0!QTOw1GiALf41n9Q?HdA&@7e#IKI&F>t~8UtO% N;OXk;vd$@?2>@XaRi^*| literal 0 HcmV?d00001 diff --git a/src/shared/libs/CometChatCalendar/Icons/index.ts b/src/shared/libs/CometChatCalendar/Icons/index.ts new file mode 100644 index 0000000..29a97cb --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Icons/index.ts @@ -0,0 +1,7 @@ +export default { + arrow: { + left: { + '16px': require('./BackArrow.png'), + }, + }, +}; diff --git a/src/shared/libs/CometChatCalendar/Locales/index.ts b/src/shared/libs/CometChatCalendar/Locales/index.ts new file mode 100644 index 0000000..ff63e5d --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Locales/index.ts @@ -0,0 +1,3 @@ +// For convenience, standardizing imports, and easy refactoring +import DefaultLocale from 'dayjs/locale/en-ca'; +export { DefaultLocale }; diff --git a/src/shared/libs/CometChatCalendar/Themes/Colors.ts b/src/shared/libs/CometChatCalendar/Themes/Colors.ts new file mode 100644 index 0000000..75ecbcf --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Themes/Colors.ts @@ -0,0 +1,31 @@ +interface Colors { + primary: string; + primaryText: string; + base: string; + baseText: string; + disabled: string; + elevation?: string; +} + +const dark: Colors = { + primary: '#FFC491', + primaryText: '#2d2d2d', + base: '#2d2d2d', + baseText: '#e6e6e6', + disabled: '#7c7c7c', + elevation: '#383838', +}; + +const light: Colors = { + primary: '#04997C', + primaryText: '#FFFFFF', + base: '#FFFFFF', + baseText: '#333333', + disabled: '#C9C9CA', + elevation: '#555555', +}; + +export default { + dark, + light, +}; diff --git a/src/shared/libs/CometChatCalendar/Themes/DarkTheme.ts b/src/shared/libs/CometChatCalendar/Themes/DarkTheme.ts new file mode 100644 index 0000000..6c56f9c --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Themes/DarkTheme.ts @@ -0,0 +1,79 @@ +import type { Theme } from '../Entities'; +import DefaultTheme from './DefaultTheme'; +import Colors from './Colors'; + +const { dark } = Colors; + +const CALENDAR_HEIGHT = 315; +const DAY_WIDTH_PERCENTAGE = 60 / 7; // 60% of width distributed among 7 weekdays +const HORIZONTAL_MARGIN_PERCENT = 40 / (7 * 2); // 40% of margin horizontal distributed among 7 weekdays +const DAY_HEIGHT_PERCENTAGE = 95 / 6; +const VERTICAL_MARGIN_PERCENT = 5 / 30; + +const DarkTheme: Theme = { + ...DefaultTheme, + calendarContainer: { + flex: 1, + minHeight: CALENDAR_HEIGHT, + backgroundColor: dark.base, + }, + titleText: { + color: dark.baseText, + letterSpacing: 0.8, + fontSize: 15, + fontWeight: '600', + }, + normalDayText: { + color: dark.baseText, + }, + normalMonthText: { + textTransform: 'uppercase', + fontSize: 12, + letterSpacing: 1, + color: dark.baseText, + alignItems: 'center', + textAlign: 'center', + fontWeight: '700', + }, + selectedMonthText: { + color: dark.primary, + }, + extraDayText: { + color: dark.disabled, + fontWeight: '300', + }, + disabledDayText: { + color: dark.disabled, + fontWeight: '300', + }, + normalArrowImage: { + height:20, + tintColor: dark.primary, + }, + normalArrowContainer: { + backgroundColor: dark.elevation, + height: 30, + width: 30, + borderRadius: 15, + justifyContent: 'center', + alignItems: 'center', + }, + selectedDayText: { + fontWeight: '700', + color: dark.primaryText, + }, + normalDayContainer: { + width: `${DAY_WIDTH_PERCENTAGE}%`, + marginHorizontal: `${HORIZONTAL_MARGIN_PERCENT}%`, + height: `${DAY_HEIGHT_PERCENTAGE}%`, + marginVertical: `${VERTICAL_MARGIN_PERCENT}%`, + justifyContent: 'center', + alignItems: 'center', + }, + selectedDayContainer: { + backgroundColor: dark.primary, + borderRadius: 50, + }, +}; + +export default DarkTheme; diff --git a/src/shared/libs/CometChatCalendar/Themes/DefaultTheme.ts b/src/shared/libs/CometChatCalendar/Themes/DefaultTheme.ts new file mode 100644 index 0000000..d965955 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Themes/DefaultTheme.ts @@ -0,0 +1,164 @@ +import { Dimensions, StyleSheet } from 'react-native'; +import type { Theme } from '../Entities'; +import Colors from './Colors'; + +const { light } = Colors; +const { width } = Dimensions.get('screen'); + +const CALENDAR_HEIGHT = 280; +const HEADER_HEIGHT = 45; +const DAY_WIDTH_PERCENTAGE = 60 / 7; // 60% of width distributed among 7 weekdays +const HORIZONTAL_MARGIN_PERCENT = 40 / (7 * 2); // 40% of margin horizontal distributed among 7 weekdays +const DAY_HEIGHT_PERCENTAGE = 92 / 6; +const VERTICAL_MARGIN_PERCENT = 3 / (6 * 2); + +const text = StyleSheet.create({ + normal: { + fontStyle: 'normal', + fontWeight: '500', + fontSize: 13, + lineHeight: 14, + alignItems: 'center', + textAlign: 'center', + letterSpacing: 0.03, + color: light.baseText, + }, + disabled: { + color: light.disabled, + }, + month: { + fontWeight: '800', + fontSize: 11, + textTransform: 'uppercase', + }, + highlighted: { + color: light.primary, + }, + title: { + fontStyle: 'normal', + fontWeight: 'bold', + letterSpacing: 0.2, + fontSize: 14, + color: light.baseText, + }, + weekday: { + width: `${DAY_WIDTH_PERCENTAGE}%`, + marginHorizontal: `${HORIZONTAL_MARGIN_PERCENT}%`, + textAlign: 'center', + textTransform: 'uppercase', + fontSize: 8, + fontWeight: 'bold', + color: light.elevation, + }, + selected: { + color: light.base, + fontWeight: '700', + }, +}); + +const container = StyleSheet.create({ + calendar: { + flex: 1, + minHeight: CALENDAR_HEIGHT, + backgroundColor: light.base, + }, + header: { + height: HEADER_HEIGHT, + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: 16, + }, + days: { + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'flex-start', + flex: 1, + }, + weekdays: { + flexDirection: 'row', + paddingVertical: 8, + }, + month: { + width: width / 3, + height: (CALENDAR_HEIGHT - HEADER_HEIGHT) / 6, + justifyContent: 'center', + }, + months: { + flexWrap: 'wrap', + flexDirection: 'row', + }, + normalDay: { + width: `${DAY_WIDTH_PERCENTAGE}%`, + marginHorizontal: `${HORIZONTAL_MARGIN_PERCENT}%`, + height: `${DAY_HEIGHT_PERCENTAGE}%`, + marginVertical: `${VERTICAL_MARGIN_PERCENT}%`, + justifyContent: 'center', + alignItems: 'center', + }, + selectedDay: { + backgroundColor: light.primary, + borderRadius: 32 / 3, + shadowColor: light.baseText, + shadowOffset: { + width: 0, + height: 1, + }, + shadowOpacity: 0.2, + shadowRadius: 1.41, + elevation: 2, + }, +}); + +const arrow = StyleSheet.create({ + normal: { + height:20, + tintColor: light.primary, + aspectRatio: 1, + }, + disabled: { + tintColor: light.disabled, + }, +}); + +const DefaultTheme: Theme = { + calendarContainer: container.calendar, + headerContainer: container.header, + normalArrowContainer: {}, + disabledArrowContainer: {}, + normalArrowImage: arrow.normal, + disabledArrowImage: arrow.disabled, + normalMonthContainer: container.month, + disabledMonthContainer: {}, + selectedMonthContainer: {}, + normalMonthText: { + ...text.normal, + ...text.month, + }, + disabledMonthText: text.disabled, + selectedMonthText: text.highlighted, + titleContainer: {}, + titleText: text.title, + weekdaysContainer: container.weekdays, + weekdayText: text.weekday, + daysContainer: container.days, + monthsContainer: container.months, + normalDayContainer: container.normalDay, + disabledDayContainer: {}, + selectedDayContainer: container.selectedDay, + extraDayContainer: {}, + startOfWeekDayContainer: {}, + endOfWeekDayContainer: {}, + startOfMonthDayContainer: {}, + endOfMonthDayContainer: {}, + normalDayText: text.normal, + disabledDayText: text.disabled, + selectedDayText: text.selected, + extraDayText: text.disabled, + startOfWeekDayText: {}, + endOfWeekDayText: {}, + startOfMonthDayText: {}, + endOfMonthDayText: {}, +}; + +export default DefaultTheme; diff --git a/src/shared/libs/CometChatCalendar/Themes/index.ts b/src/shared/libs/CometChatCalendar/Themes/index.ts new file mode 100644 index 0000000..8c35bd4 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Themes/index.ts @@ -0,0 +1,3 @@ +export { default as DefaultTheme } from './DefaultTheme'; +export { default as LightTheme } from './DefaultTheme'; +export { default as DarkTheme } from './DarkTheme'; diff --git a/src/shared/libs/CometChatCalendar/Utils/addOpacity.test.ts b/src/shared/libs/CometChatCalendar/Utils/addOpacity.test.ts new file mode 100644 index 0000000..43937af --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/addOpacity.test.ts @@ -0,0 +1,49 @@ +import addOpacity from './addOpacity'; + +describe('Converts color #rrggbb to #rrggbbaa', () => { + test('Should return #0000001A for opacity 0.1 (10%)', () => { + const color = '#000000'; + const opacity = 0.1; + const after = '#0000001A'; + expect(addOpacity(color, opacity)).toBe(after); + }); + test('Should return #00000040 for opacity 0.25 (25%)', () => { + const color = '#000000'; + const opacity = 0.25; + const after = '#00000040'; + expect(addOpacity(color, opacity)).toBe(after); + }); + test('Should return #0000007F for opacity 0.5 (50%)', () => { + const color = '#000000'; + const opacity = 0.5; + const after = '#00000080'; + expect(addOpacity(color, opacity)).toBe(after); + }); + test('Should return #000000BF for opacity 0.75 (75%)', () => { + const color = '#000000'; + const opacity = 0.75; + const after = '#000000BF'; + expect(addOpacity(color, opacity)).toBe(after); + }); + test('Should return #000000FF for opacity 1 (100%)', () => { + const color = '#000000'; + const opacity = 1; + const after = '#000000FF'; + expect(addOpacity(color, opacity)).toBe(after); + }); +}); + +describe('Handle opacity out of range (less then 0 or greater than 1)', () => { + test('Should return #000000FF for an opacity greater than 1', () => { + const color = '#000000'; + const opacity = 100; + const after = '#000000FF'; + expect(addOpacity(color, opacity)).toBe(after); + }); + test('Should return #000000FF for an opacity less than 0', () => { + const color = '#000000'; + const opacity = -20; + const after = '#00000000'; + expect(addOpacity(color, opacity)).toBe(after); + }); +}); diff --git a/src/shared/libs/CometChatCalendar/Utils/addOpacity.ts b/src/shared/libs/CometChatCalendar/Utils/addOpacity.ts new file mode 100644 index 0000000..c27d006 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/addOpacity.ts @@ -0,0 +1,15 @@ +import clamp from './clamp'; + +const addOpacity = (color: string, opacity: number): string => { + const normalizedOpacity = clamp(opacity, 0, 1); + + const alpha = Math.round(255 * normalizedOpacity) + .toString(16) + .toUpperCase(); + + const normalizedAlpha = alpha.padStart(2, '0'); + + return `${color}${normalizedAlpha}`; +}; + +export default addOpacity; diff --git a/src/shared/libs/CometChatCalendar/Utils/checkChangedProps.ts b/src/shared/libs/CometChatCalendar/Utils/checkChangedProps.ts new file mode 100644 index 0000000..38cc9ed --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/checkChangedProps.ts @@ -0,0 +1,10 @@ +export default (prevProps: Record<string, any>, nextProps: Record<string, any>) => { + Object.keys(nextProps) + .filter((key) => { + return nextProps[key] !== prevProps[key]; + }) + .map((key) => { + console.log('changed property:', key, 'from', prevProps[key], 'to', nextProps[key]); + }); + return false; +}; diff --git a/src/shared/libs/CometChatCalendar/Utils/clamp.test.ts b/src/shared/libs/CometChatCalendar/Utils/clamp.test.ts new file mode 100644 index 0000000..ea6a48e --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/clamp.test.ts @@ -0,0 +1,39 @@ +import clamp from './clamp'; + +describe('works in the positive ranges', () => { + test('returns the input in range min..max', () => { + expect(clamp(10, 10, 20)).toBe(10); + expect(clamp(20, 10, 20)).toBe(20); + }); + + test('returns the maximum for values greater than the maximum', () => { + expect(clamp(21, 10, 20)).toBe(20); + expect(clamp(200, 10, 20)).toBe(20); + expect(clamp(20000, 10, 20)).toBe(20); + }); + + test('returns the minimum for values less than the minimum', () => { + expect(clamp(9, 10, 20)).toBe(10); + expect(clamp(0, 10, 20)).toBe(10); + expect(clamp(-10, 10, 20)).toBe(10); + }); +}); + +describe('works in negative ranges', () => { + test('returns the input in range min..max', () => { + expect(clamp(-100, -100, -20)).toBe(-100); + expect(clamp(-20, -100, -20)).toBe(-20); + }); + + test('returns the maximum for values greater than the maximum', () => { + expect(clamp(-9, -20, -10)).toBe(-10); + expect(clamp(0, -20, -10)).toBe(-10); + expect(clamp(200, -20, -10)).toBe(-10); + }); + + test('returns the minimum for values less than the minimum', () => { + expect(clamp(-21, -20, -10)).toBe(-20); + expect(clamp(-100, -20, -10)).toBe(-20); + expect(clamp(-10000, -20, -10)).toBe(-20); + }); +}); diff --git a/src/shared/libs/CometChatCalendar/Utils/clamp.ts b/src/shared/libs/CometChatCalendar/Utils/clamp.ts new file mode 100644 index 0000000..960bc69 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/clamp.ts @@ -0,0 +1,5 @@ +const clamp = (val: number, min: number, max: number) => { + return Math.min(Math.max(min, val), max); +}; + +export default clamp; diff --git a/src/shared/libs/CometChatCalendar/Utils/dateRange.test.ts b/src/shared/libs/CometChatCalendar/Utils/dateRange.test.ts new file mode 100644 index 0000000..6929f76 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/dateRange.test.ts @@ -0,0 +1,20 @@ +import dateRange from './dateRange'; +import dayjs from 'dayjs'; + +test('works for 3 days', () => { + const start = dayjs(); + const end = dayjs().add(2, 'day'); + expect(dateRange(start, end).length).toBe(3); +}); + +test('works for 1 day', () => { + const start = dayjs(); + const end = dayjs(); + expect(dateRange(start, end).length).toBe(1); +}); + +test("throws error if start date doesn't come before end date", () => { + const start = dayjs(); + const end = dayjs().subtract(2, 'day'); + expect(() => dateRange(start, end)).toThrow('Start date must come before end date'); +}); diff --git a/src/shared/libs/CometChatCalendar/Utils/dateRange.ts b/src/shared/libs/CometChatCalendar/Utils/dateRange.ts new file mode 100644 index 0000000..fd507e5 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/dateRange.ts @@ -0,0 +1,22 @@ +import type { Dayjs } from 'dayjs'; + +const dateRange = (start: Dayjs, end: Dayjs, jump: 'day' | 'month' = 'day') => { + let range: Dayjs[] = []; + let current: Dayjs = start; + + if (end.isBefore(start)) { + throw new Error('Start date must come before end date'); + } + + // To avoid loading isSameOrBefore plugin, we add a day to end date + let _end = end.add(1, jump); + + while (!current.isSame(_end, jump)) { + range.push(current); + current = current.add(1, jump); + } + + return range; +}; + +export default dateRange; diff --git a/src/shared/libs/CometChatCalendar/Utils/getExtraDays.test.ts b/src/shared/libs/CometChatCalendar/Utils/getExtraDays.test.ts new file mode 100644 index 0000000..8e21c50 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/getExtraDays.test.ts @@ -0,0 +1,48 @@ +import dayjs from 'dayjs'; +import getExtraDays from './getExtraDays'; + +test('Negative range - "to" is before "from"', () => { + const result = getExtraDays({ + from: dayjs('2020-01-02'), + to: dayjs('2020-01-01'), + }); + + expect(result).toEqual([]); +}); + +describe('Positive range - "to" is equal to/after "from"', () => { + test('1 day', () => { + const result = getExtraDays({ + from: dayjs('2020-01-01'), + to: dayjs('2020-01-01'), + }); + + expect(result.length).toBe(1); + expect(result[0].isSame('2020-01-01', 'day')).toBeTruthy(); + }); + + test('2 days', () => { + const result = getExtraDays({ + from: dayjs('2020-01-01'), + to: dayjs('2020-01-02'), + }); + + expect(result.length).toBe(2); + expect(result[0].isSame('2020-01-01', 'day')).toBeTruthy(); + expect(result[1].isSame('2020-01-02', 'day')).toBeTruthy(); + }); + + test('5 days', () => { + const result = getExtraDays({ + from: dayjs('2020-01-01'), + to: dayjs('2020-01-05'), + }); + + expect(result.length).toBe(5); + expect(result[0].isSame('2020-01-01', 'day')).toBeTruthy(); + expect(result[1].isSame('2020-01-02', 'day')).toBeTruthy(); + expect(result[2].isSame('2020-01-03', 'day')).toBeTruthy(); + expect(result[3].isSame('2020-01-04', 'day')).toBeTruthy(); + expect(result[4].isSame('2020-01-05', 'day')).toBeTruthy(); + }); +}); diff --git a/src/shared/libs/CometChatCalendar/Utils/getExtraDays.ts b/src/shared/libs/CometChatCalendar/Utils/getExtraDays.ts new file mode 100644 index 0000000..296daf3 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/getExtraDays.ts @@ -0,0 +1,24 @@ +import type { Dayjs } from 'dayjs'; + +// Inclusive range +const getExtraDays = ({ from, to }: { from: Dayjs; to: Dayjs }): Dayjs[] => { + const days: Dayjs[] = []; + const diff = from.diff(to, 'day'); + + // If 'to' date is before 'from' date, there are 0 slots available + if (diff > 0) { + return days; + } + + /* + * Start iteration from 0 to include 'from' day + * Iterate until less than or equal to diff, to include 'to' day + */ + + for (let i = 0; i <= Math.abs(diff); i += 1) { + days.push(from.add(i, 'day')); + } + return days; +}; + +export default getExtraDays; diff --git a/src/shared/libs/CometChatCalendar/Utils/getSurroundingTimeUnits.test.ts b/src/shared/libs/CometChatCalendar/Utils/getSurroundingTimeUnits.test.ts new file mode 100644 index 0000000..b8a3449 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/getSurroundingTimeUnits.test.ts @@ -0,0 +1,60 @@ +import getSurroundingTimeUnits from './getSurroundingTimeUnits'; + +describe('Surrounding months', () => { + describe('works for months surrouding Febuary', () => { + test('January', () => { + const { month } = getSurroundingTimeUnits('2020-01-25'); + expect(month.last.format('YYYY-MM-DD')).toEqual('2019-12-31'); + expect(month.next.format('YYYY-MM-DD')).toEqual('2020-02-01'); + }); + test('March', () => { + const { month } = getSurroundingTimeUnits('2020-03-25'); + expect(month.last.format('YYYY-MM-DD')).toEqual('2020-02-29'); + expect(month.next.format('YYYY-MM-DD')).toEqual('2020-04-01'); + }); + }); + + test('works for end-of-month visible date', () => { + const { month } = getSurroundingTimeUnits('2020-07-31'); + expect(month.last.format('YYYY-MM-DD')).toEqual('2020-06-30'); + expect(month.next.format('YYYY-MM-DD')).toEqual('2020-08-01'); + }); + test('works for start-of-month visible date', () => { + const { month } = getSurroundingTimeUnits('2020-07-01'); + expect(month.last.format('YYYY-MM-DD')).toEqual('2020-06-30'); + expect(month.next.format('YYYY-MM-DD')).toEqual('2020-08-01'); + }); + test('works for end-of-year visible dates', () => { + const { month } = getSurroundingTimeUnits('2020-12-31'); + expect(month.last.format('YYYY-MM-DD')).toEqual('2020-11-30'); + expect(month.next.format('YYYY-MM-DD')).toEqual('2021-01-01'); + }); + test('works for start-of-year visible dates', () => { + const { month } = getSurroundingTimeUnits('2020-01-01'); + expect(month.last.format('YYYY-MM-DD')).toEqual('2019-12-31'); + expect(month.next.format('YYYY-MM-DD')).toEqual('2020-02-01'); + }); +}); + +describe('Surrounding years', () => { + test('works for end-of-month visible date', () => { + const { year } = getSurroundingTimeUnits('2020-07-31'); + expect(year.last.format('YYYY-MM-DD')).toEqual('2019-12-31'); + expect(year.next.format('YYYY-MM-DD')).toEqual('2021-01-01'); + }); + test('works for start-of-month visible date', () => { + const { year } = getSurroundingTimeUnits('2020-07-01'); + expect(year.last.format('YYYY-MM-DD')).toEqual('2019-12-31'); + expect(year.next.format('YYYY-MM-DD')).toEqual('2021-01-01'); + }); + test('works for end-of-year visible dates', () => { + const { year } = getSurroundingTimeUnits('2020-12-31'); + expect(year.last.format('YYYY-MM-DD')).toEqual('2019-12-31'); + expect(year.next.format('YYYY-MM-DD')).toEqual('2021-01-01'); + }); + test('works for start-of-year visible dates', () => { + const { year } = getSurroundingTimeUnits('2020-01-01'); + expect(year.last.format('YYYY-MM-DD')).toEqual('2019-12-31'); + expect(year.next.format('YYYY-MM-DD')).toEqual('2021-01-01'); + }); +}); diff --git a/src/shared/libs/CometChatCalendar/Utils/getSurroundingTimeUnits.ts b/src/shared/libs/CometChatCalendar/Utils/getSurroundingTimeUnits.ts new file mode 100644 index 0000000..1f898dd --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/getSurroundingTimeUnits.ts @@ -0,0 +1,19 @@ +import dayjs from 'dayjs'; + +const getSurroundingTimeUnits = (visibleDate: string) => { + const month = { + next: dayjs(visibleDate).startOf('month').add(1, 'month'), + last: dayjs(visibleDate).endOf('month').subtract(1, 'month'), + }; + const year = { + next: dayjs(visibleDate).startOf('year').add(1, 'year'), + last: dayjs(visibleDate).endOf('year').subtract(1, 'year'), + }; + + return { + year, + month, + }; +}; + +export default getSurroundingTimeUnits; diff --git a/src/shared/libs/CometChatCalendar/Utils/index.ts b/src/shared/libs/CometChatCalendar/Utils/index.ts new file mode 100644 index 0000000..cea9fc8 --- /dev/null +++ b/src/shared/libs/CometChatCalendar/Utils/index.ts @@ -0,0 +1,6 @@ +export { default as addOpacity } from './addOpacity'; +export { default as getSurroundingTimeUnits } from './getSurroundingTimeUnits'; +export { default as getExtraDays } from './getExtraDays'; +export { default as clamp } from './clamp'; +export { default as dateRange } from './dateRange'; +export { default as checkChangedProps } from './checkChangedProps'; diff --git a/src/shared/libs/CometChatCalendar/index.ts b/src/shared/libs/CometChatCalendar/index.ts new file mode 100644 index 0000000..619261d --- /dev/null +++ b/src/shared/libs/CometChatCalendar/index.ts @@ -0,0 +1,5 @@ +export * from './Calendars'; +export * from './Themes'; +export * from './Components'; +export * from './Entities'; +export * from './Constants'; diff --git a/src/shared/libs/datePickerModal/DateTimePickerModal.android.js b/src/shared/libs/datePickerModal/DateTimePickerModal.android.js new file mode 100644 index 0000000..3495d1b --- /dev/null +++ b/src/shared/libs/datePickerModal/DateTimePickerModal.android.js @@ -0,0 +1,81 @@ +import React, { useEffect, useRef, useState, memo } from "react"; +import PropTypes from "prop-types"; +import DateTimePicker from "@react-native-community/datetimepicker"; + +// Memo workaround for https://github.com/react-native-community/datetimepicker/issues/54 +const areEqual = (prevProps, nextProps) => { + return ( + prevProps.isVisible === nextProps.isVisible && + prevProps.date.getTime() === nextProps.date.getTime() + ); +}; + +const DateTimePickerModal = memo( + ({ date, mode, isVisible, onCancel, onConfirm, onHide, ...otherProps }) => { + const currentDateRef = useRef(date); + const [currentMode, setCurrentMode] = useState(null); + + useEffect(() => { + if (isVisible && currentMode === null) { + setCurrentMode(mode === "time" ? "time" : "date"); + } else if (!isVisible) { + setCurrentMode(null); + } + }, [isVisible, currentMode, mode]); + + if (!isVisible || !currentMode) return null; + + const handleChange = (event, date) => { + if (event.type === "dismissed") { + onCancel(); + onHide(false); + return; + } + let nextDate = date; + if (mode === "datetime") { + if (currentMode === "date") { + setCurrentMode("time"); + currentDateRef.current = new Date(date); + return; + } else if (currentMode === "time") { + const year = currentDateRef.current.getFullYear(); + const month = currentDateRef.current.getMonth(); + const day = currentDateRef.current.getDate(); + const hours = date.getHours(); + const minutes = date.getMinutes(); + nextDate = new Date(year, month, day, hours, minutes); + } + } + onConfirm(nextDate); + onHide(true, nextDate); + }; + + return ( + <DateTimePicker + {...otherProps} + mode={currentMode} + value={date} + onChange={handleChange} + /> + ); + }, + areEqual +); + +DateTimePickerModal.propTypes = { + date: PropTypes.instanceOf(Date), + isVisible: PropTypes.bool, + onCancel: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + onHide: PropTypes.func, + maximumDate: PropTypes.instanceOf(Date), + minimumDate: PropTypes.instanceOf(Date), +}; + +DateTimePickerModal.defaultProps = { + date: new Date(), + isVisible: false, + onHide: () => {}, +}; + +export { DateTimePickerModal }; diff --git a/src/shared/libs/datePickerModal/DateTimePickerModal.ios.js b/src/shared/libs/datePickerModal/DateTimePickerModal.ios.js new file mode 100644 index 0000000..9a03498 --- /dev/null +++ b/src/shared/libs/datePickerModal/DateTimePickerModal.ios.js @@ -0,0 +1,363 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { + StyleSheet, + Text, + TouchableHighlight, + View, + Appearance, +} from "react-native"; +import DateTimePicker from "@react-native-community/datetimepicker"; +import Modal from "./Modal"; +import { isIphoneX } from "./utils"; +import DateTime from "../luxon/src/datetime"; + +export const BACKGROUND_COLOR_LIGHT = "white"; +export const BACKGROUND_COLOR_DARK = "#0E0E0E"; +export const BORDER_COLOR = "#d5d5d5"; +export const BORDER_COLOR_DARK = "#272729"; +export const BORDER_RADIUS = 13; +export const BUTTON_FONT_WEIGHT = "normal"; +export const BUTTON_FONT_COLOR = "#007ff9"; +export const BUTTON_FONT_SIZE = 20; +export const HIGHLIGHT_COLOR_DARK = "#444444"; +export const HIGHLIGHT_COLOR_LIGHT = "#ebebeb"; + +export class DateTimePickerModal extends React.PureComponent { + static propTypes = { + buttonTextColorIOS: PropTypes.string, + cancelButtonTestID: PropTypes.string, + confirmButtonTestID: PropTypes.string, + cancelTextIOS: PropTypes.string, + confirmTextIOS: PropTypes.string, + customCancelButtonIOS: PropTypes.elementType, + customConfirmButtonIOS: PropTypes.elementType, + customHeaderIOS: PropTypes.elementType, + customPickerIOS: PropTypes.elementType, + date: PropTypes.instanceOf(Date), + modalPropsIOS: PropTypes.any, + modalStyleIOS: PropTypes.any, + isDarkModeEnabled: PropTypes.bool, + isVisible: PropTypes.bool, + pickerContainerStyleIOS: PropTypes.any, + pickerStyleIOS: PropTypes.any, + backdropStyleIOS: PropTypes.any, + pickerComponentStyleIOS: PropTypes.any, + onCancel: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + onChange: PropTypes.func, + onHide: PropTypes.func, + maximumDate: PropTypes.instanceOf(Date), + minimumDate: PropTypes.instanceOf(Date), + }; + + static defaultProps = { + cancelTextIOS: "Cancel", + confirmTextIOS: "Confirm", + modalPropsIOS: {}, + date: new Date(), + isDarkModeEnabled: undefined, + isVisible: false, + pickerContainerStyleIOS: {}, + pickerStyleIOS: {}, + backdropStyleIOS: {}, + pickerComponentStyleIOS: {}, + }; + + state = { + currentDate: this.props.date, + isPickerVisible: this.props.isVisible, + }; + + didPressConfirm = false; + + static getDerivedStateFromProps(props, state) { + if (props.isVisible && !state.isPickerVisible) { + return { currentDate: props.date, isPickerVisible: true }; + } + return null; + } + + handleCancel = () => { + this.didPressConfirm = false; + this.props.onCancel(); + }; + + handleConfirm = () => { + this.didPressConfirm = true; + this.props.onConfirm(this.state.currentDate); + }; + + handleHide = () => { + const { onHide } = this.props; + if (onHide) { + onHide(this.didPressConfirm, this.state.currentDate); + } + this.setState({ isPickerVisible: false }); + }; + + handleChange = (event, date) => { + console.log('handleChange',event.timeStamp,DateTime.fromJSDate(date).toFormat("HH:mm:ss")); + if (this.props.onChange) { + this.props.onChange(date); + } + this.setState({ currentDate: new Date(date) }); + }; + + render() { + const { + cancelButtonTestID, + confirmButtonTestID, + cancelTextIOS, + confirmTextIOS, + customCancelButtonIOS, + customConfirmButtonIOS, + customHeaderIOS, + customPickerIOS, + date, + display, + isDarkModeEnabled, + isVisible, + modalStyleIOS, + modalPropsIOS, + pickerContainerStyleIOS, + pickerStyleIOS, + pickerComponentStyleIOS, + onCancel, + onConfirm, + onChange, + onHide, + backdropStyleIOS, + buttonTextColorIOS, + ...otherProps + } = this.props; + const isAppearanceModuleAvailable = !!( + Appearance && Appearance.getColorScheme + ); + const _isDarkModeEnabled = + isDarkModeEnabled === undefined && isAppearanceModuleAvailable + ? Appearance.getColorScheme() === "dark" + : isDarkModeEnabled || false; + + const ConfirmButtonComponent = customConfirmButtonIOS || ConfirmButton; + const CancelButtonComponent = customCancelButtonIOS || CancelButton; + const PickerComponent = customPickerIOS || DateTimePicker; + const HeaderComponent = customHeaderIOS; + + const themedContainerStyle = _isDarkModeEnabled + ? pickerStyles.containerDark + : pickerStyles.containerLight; + + return ( + <Modal + isVisible={isVisible} + contentStyle={[pickerStyles.modal, modalStyleIOS]} + onBackdropPress={this.handleCancel} + onHide={this.handleHide} + backdropStyle={backdropStyleIOS} + {...modalPropsIOS} + > + <View + style={[ + pickerStyles.container, + themedContainerStyle, + pickerContainerStyleIOS, + ]} + > + {HeaderComponent && <HeaderComponent />} + {!HeaderComponent && display === "inline" && ( + <View style={pickerStyles.headerFiller} /> + )} + <View + style={[ + display === "inline" + ? pickerStyles.pickerInline + : pickerStyles.pickerSpinner, + pickerStyleIOS, + ]} + > + <PickerComponent + display={display || "spinner"} + {...otherProps} + value={this.state.currentDate} + onChange={this.handleChange} + // Recent versions @react-native-community/datetimepicker (at least starting with 6.7.0) + // have a peculiar iOS behaviour where sometimes, for example in react-native Modal, + // the inline picker is not rendered correctly if in datetime mode. Explicitly setting the height + // of the native picker to 370 fixes this issue. This is dependent on the other styles applied to the picker + // and may need to be adjusted if the other styles are changed. + style={[ + { + height: + !customPickerIOS && + otherProps.mode === "datetime" && + display === "inline" + ? 380 + : undefined, + }, + pickerComponentStyleIOS, + ]} + /> + </View> + <ConfirmButtonComponent + confirmButtonTestID={confirmButtonTestID} + isDarkModeEnabled={_isDarkModeEnabled} + onPress={this.handleConfirm} + label={confirmTextIOS} + buttonTextColorIOS={buttonTextColorIOS} + /> + </View> + <CancelButtonComponent + cancelButtonTestID={cancelButtonTestID} + isDarkModeEnabled={_isDarkModeEnabled} + onPress={this.handleCancel} + label={cancelTextIOS} + buttonTextColorIOS={buttonTextColorIOS} + /> + </Modal> + ); + } +} + +const pickerStyles = StyleSheet.create({ + modal: { + justifyContent: "flex-end", + margin: 10, + marginBottom: isIphoneX() ? 34 : 10, + }, + container: { + borderRadius: BORDER_RADIUS, + marginBottom: 8, + overflow: "hidden", + }, + pickerSpinner: { + marginBottom: 8, + }, + pickerInline: { + paddingHorizontal: 12, + paddingTop: 14, + }, + containerLight: { + backgroundColor: BACKGROUND_COLOR_LIGHT, + }, + containerDark: { + backgroundColor: BACKGROUND_COLOR_DARK, + }, +}); + +export const ConfirmButton = ({ + isDarkModeEnabled, + confirmButtonTestID, + onPress, + label, + buttonTextColorIOS, + style = confirmButtonStyles, +}) => { + const themedButtonStyle = isDarkModeEnabled + ? confirmButtonStyles.buttonDark + : confirmButtonStyles.buttonLight; + + const underlayColor = isDarkModeEnabled + ? HIGHLIGHT_COLOR_DARK + : HIGHLIGHT_COLOR_LIGHT; + return ( + <TouchableHighlight + testID={confirmButtonTestID} + style={[themedButtonStyle, style.button]} + underlayColor={underlayColor} + onPress={onPress} + accessible={true} + accessibilityRole="button" + accessibilityLabel={label} + > + <Text + style={[ + style.text, + buttonTextColorIOS && { color: buttonTextColorIOS }, + ]} + > + {label} + </Text> + </TouchableHighlight> + ); +}; + +export const confirmButtonStyles = StyleSheet.create({ + button: { + borderTopWidth: StyleSheet.hairlineWidth, + backgroundColor: "transparent", + height: 57, + justifyContent: "center", + }, + buttonLight: { + borderColor: BORDER_COLOR, + }, + buttonDark: { + borderColor: BORDER_COLOR_DARK, + }, + text: { + textAlign: "center", + color: BUTTON_FONT_COLOR, + fontSize: BUTTON_FONT_SIZE, + fontWeight: BUTTON_FONT_WEIGHT, + backgroundColor: "transparent", + }, +}); + +export const CancelButton = ({ + cancelButtonTestID, + isDarkModeEnabled, + onPress, + label, + buttonTextColorIOS, + style = cancelButtonStyles, +}) => { + const themedButtonStyle = isDarkModeEnabled + ? cancelButtonStyles.buttonDark + : cancelButtonStyles.buttonLight; + const underlayColor = isDarkModeEnabled + ? HIGHLIGHT_COLOR_DARK + : HIGHLIGHT_COLOR_LIGHT; + return ( + <TouchableHighlight + testID={cancelButtonTestID} + style={[themedButtonStyle, style.button]} + underlayColor={underlayColor} + onPress={onPress} + accessible={true} + accessibilityRole="button" + accessibilityLabel={label} + > + <Text + style={[ + style.text, + buttonTextColorIOS && { color: buttonTextColorIOS }, + ]} + > + {label} + </Text> + </TouchableHighlight> + ); +}; + +export const cancelButtonStyles = StyleSheet.create({ + button: { + borderRadius: BORDER_RADIUS, + height: 57, + justifyContent: "center", + }, + buttonLight: { + backgroundColor: BACKGROUND_COLOR_LIGHT, + }, + buttonDark: { + backgroundColor: BACKGROUND_COLOR_DARK, + }, + text: { + padding: 10, + textAlign: "center", + color: BUTTON_FONT_COLOR, + fontSize: BUTTON_FONT_SIZE, + fontWeight: "600", + backgroundColor: "transparent", + }, +}); diff --git a/src/shared/libs/datePickerModal/DateTimePickerModal.js b/src/shared/libs/datePickerModal/DateTimePickerModal.js new file mode 100644 index 0000000..18bd621 --- /dev/null +++ b/src/shared/libs/datePickerModal/DateTimePickerModal.js @@ -0,0 +1,9 @@ +import React from "react"; +import { Platform } from "react-native"; + +export function DateTimePickerModal() { + React.useEffect(() => { + console.warn(`DateTimePicker is not supported on: ${Platform.OS}`); + }, []); + return null; +} diff --git a/src/shared/libs/datePickerModal/Modal.js b/src/shared/libs/datePickerModal/Modal.js new file mode 100644 index 0000000..ab94984 --- /dev/null +++ b/src/shared/libs/datePickerModal/Modal.js @@ -0,0 +1,180 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { + Animated, + DeviceEventEmitter, + Dimensions, + Easing, + Modal as ReactNativeModal, + StyleSheet, + TouchableWithoutFeedback, +} from "react-native"; + +const MODAL_ANIM_DURATION = 300; +const MODAL_BACKDROP_OPACITY = 0.4; + +export class Modal extends Component { + static propTypes = { + onBackdropPress: PropTypes.func, + onHide: PropTypes.func, + isVisible: PropTypes.bool, + contentStyle: PropTypes.any, + }; + + static defaultProps = { + onBackdropPress: () => null, + onHide: () => null, + isVisible: false, + }; + + state = { + isVisible: this.props.isVisible, + deviceWidth: Dimensions.get("window").width, + deviceHeight: Dimensions.get("window").height, + }; + + animVal = new Animated.Value(0); + _isMounted = false; + + static _deviceEventEmitter = null; + + componentDidMount() { + this._isMounted = true; + if (this.state.isVisible) { + this.show(); + } + this._deviceEventEmitter = DeviceEventEmitter.addListener( + "didUpdateDimensions", + this.handleDimensionsUpdate + ); + } + + componentWillUnmount() { + this._deviceEventEmitter.remove(); + this._isMounted = false; + } + + componentDidUpdate(prevProps: ModalPropsType) { + if (this.props.isVisible && !prevProps.isVisible) { + this.show(); + } else if (!this.props.isVisible && prevProps.isVisible) { + this.hide(); + } + } + + handleDimensionsUpdate = (dimensionsUpdate) => { + const deviceWidth = dimensionsUpdate.window.width; + const deviceHeight = dimensionsUpdate.window.height; + if ( + deviceWidth !== this.state.deviceWidth || + deviceHeight !== this.state.deviceHeight + ) { + this.setState({ deviceWidth, deviceHeight }); + } + }; + + show = () => { + this.setState({ isVisible: true }); + Animated.timing(this.animVal, { + easing: Easing.inOut(Easing.quad), + // Using native driver in the modal makes the content flash + useNativeDriver: false, + duration: MODAL_ANIM_DURATION, + toValue: 1, + }).start(); + }; + + hide = () => { + Animated.timing(this.animVal, { + easing: Easing.inOut(Easing.quad), + // Using native driver in the modal makes the content flash + useNativeDriver: false, + duration: MODAL_ANIM_DURATION, + toValue: 0, + }).start(() => { + if (this._isMounted) { + this.setState({ isVisible: false }, this.props.onHide); + } + }); + }; + + render() { + const { + children, + onBackdropPress, + contentStyle, + backdropStyle, + ...otherProps + } = this.props; + const { deviceHeight, deviceWidth, isVisible } = this.state; + const backdropAnimatedStyle = { + opacity: this.animVal.interpolate({ + inputRange: [0, 1], + outputRange: [0, MODAL_BACKDROP_OPACITY], + }), + }; + const contentAnimatedStyle = { + transform: [ + { + translateY: this.animVal.interpolate({ + inputRange: [0, 1], + outputRange: [deviceHeight, 0], + extrapolate: "clamp", + }), + }, + ], + }; + return ( + <ReactNativeModal + transparent + animationType="none" + visible={isVisible} + {...otherProps} + > + <TouchableWithoutFeedback onPress={onBackdropPress}> + <Animated.View + style={[ + styles.backdrop, + backdropAnimatedStyle, + { width: deviceWidth, height: deviceHeight }, + backdropStyle, + ]} + /> + </TouchableWithoutFeedback> + {isVisible && ( + <Animated.View + style={[styles.content, contentAnimatedStyle, contentStyle]} + pointerEvents="box-none" + > + {children} + </Animated.View> + )} + </ReactNativeModal> + ); + } +} + +const styles = StyleSheet.create({ + container: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + backdrop: { + position: "absolute", + top: 0, + bottom: 0, + left: 0, + right: 0, + backgroundColor: "black", + opacity: 0, + }, + content: { + flex: 1, + justifyContent: "flex-end", + }, +}); + +export default Modal; diff --git a/src/shared/libs/datePickerModal/index.d.ts b/src/shared/libs/datePickerModal/index.d.ts new file mode 100644 index 0000000..c03b792 --- /dev/null +++ b/src/shared/libs/datePickerModal/index.d.ts @@ -0,0 +1,310 @@ +// Type definitions for react-native-modal-datetime-picker +// Project: https://github.com/mmazzarolo/react-native-modal-datetime-picker +// Definitions by: +// Kyle Roach <https://github.com/iRoachie> +// Michiel De Mey <https://github.com/MichielDeMey> +// TypeScript Version: 3.5 + +import * as React from "react"; +import { ViewStyle } from "react-native"; +import { + IOSNativeProps, + AndroidNativeProps, +} from "@react-native-community/datetimepicker"; + +export type CancelButtonStylePropTypes = { + button: { + borderRadius: number, + height: number | string, + marginBottom: number | string, + justifyContent: string, + }, + buttonLight: { + backgroundColor: string, + }, + buttonDark: { + backgroundColor: string, + }, + text: { + padding: number | string, + textAlign: string, + color: string, + fontSize: number, + fontWeight: string, + backgroundColor: string, + }, +}; + +export type ConfirmButtonStylePropTypes = { + button: { + borderTopWidth: number, + backgroundColor: string, + height: number | string, + justifyContent: string, + }, + buttonLight: { + borderColor: string, + }, + buttonDark: { + borderColor: string, + }, + text: { + textAlign: string, + color: string, + fontSize: number, + fontWeight: string, + backgroundColor: string, + }, +}; + +export type CancelButtonPropTypes = { + isDarkModeEnabled?: boolean, + cancelButtonTestID?: string, + onPress: () => void, + label: string, + buttonTextColorIOS?: string, + style?: CancelButtonStylePropTypes, +}; + +export type ConfirmButtonPropTypes = { + isDarkModeEnabled?: boolean, + confirmButtonTestID?: string, + onPress: () => void, + label: string, + buttonTextColorIOS?: string, + style?: ConfirmButtonStylePropTypes, +}; + +export type CustomCancelButtonPropTypes = { + isDarkModeEnabled?: boolean, + onPress: () => void, + label: string, +}; + +export type CustomConfirmButtonPropTypes = { + isDarkModeEnabled?: boolean, + onPress: () => void, + label: string, +}; + +export type HeaderComponent = React.ComponentType<{ + label: string; +}>; + +export type PickerComponent = React.ComponentType<IOSNativeProps>; + +export interface DateTimePickerProps { + /** + * iOS buttons text color + * + * Default is '#007ff9' + */ + buttonTextColorIOS?: string; + + /** + * The prop to locate cancel button for e2e testing + */ + cancelButtonTestID?: string; + + /** + * The prop to locate confirm button for e2e testing + */ + confirmButtonTestID?: string; + + /** + * The text on the cancel button on iOS + * + * Default is 'Cancel' + */ + cancelTextIOS?: string; + + /** + * The text on the confirm button on iOS + * + * Default is 'Confirm' + */ + confirmTextIOS?: string; + + /** + * A custom component for the cancel button on iOS + */ + customCancelButtonIOS?: React.FunctionComponent<CustomCancelButtonPropTypes>; + + /** + * A custom component for the confirm button on iOS + */ + customConfirmButtonIOS?: React.FunctionComponent<CustomConfirmButtonPropTypes>; + + /** + * A custom component for the title container on iOS + */ + customHeaderIOS?: HeaderComponent; + + /** + * A custom component that will replace the default DatePicker on iOS + */ + customPickerIOS?: PickerComponent; + + /** + * Style of the backgrop (iOS) + */ + backdropStyleIOS?: ViewStyle; + + /** + * Style of the modal content (iOS) + */ + modalStyleIOS?: ViewStyle; + + /** + * The style of the picker container (iOS) + */ + pickerContainerStyleIOS?: ViewStyle; + + /** + * The style applied to the actual picker component - this can be + * either a native iOS picker or a custom one if `customPickerIOS` was provided + */ + pickerComponentStyleIOS?: ViewStyle; + + /** + * Initial selected date/time + * + * Default is a date object from `new Date()` + */ + date?: Date; + + /** + * The date picker locale. + */ + locale?: string; + + /** + * Toggles the dark mode style of the picker + * If not set, the picker tries to use the color-scheme from the Appearance module, if available. + * + * Default is undefined + */ + isDarkModeEnabled?: boolean; + + /** + * Sets the visibility of the picker + * + * Default is false + */ + isVisible?: boolean; + + /** + * Sets mode to 24 hour time + * If false, the picker shows an AM/PM chooser on Android + * + * Default is true + */ + is24Hour?: boolean; + + /** + * The mode of the picker + * + * Available modes are: + * date - Shows Datepicker + * time - Shows Timepicker + * datetime - Shows a combined Date and Time Picker + * + * Default is 'date' + */ + mode?: "date" | "time" | "datetime"; + + /** + * Additional modal props for iOS. + * + * See https://reactnative.dev/docs/modal for the available props. + */ + modalPropsIOS?: Object; + + /** + * Toggles the time mode on Android between spinner and clock views + * + * Default is 'default' which shows either spinner or clock based on Android version + */ + timePickerModeAndroid?: "spinner" | "clock" | "default"; + + /** + * Minimum date the picker can go back to + */ + minimumDate?: Date; + + /** + * Maximum date the picker can go forward to + */ + maximumDate?: Date; + + /** + * enum(1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30) + * The interval at which minutes can be selected. + * + * @extends from DatePickerIOSProperties + */ + minuteInterval?: 1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 15 | 20 | 30; + + /** + * Timezone offset in minutes. + * By default, the date picker will use the device's timezone. With this parameter, it is possible to force a certain timezone offset. + * For instance, to show times in Pacific Standard Time, pass -7 * 60. + * + * @extends from DatePickerIOSProperties + */ + timeZoneOffsetInMinutes?: number; + + /** + * Date change handler. + * This is called when the user changes the date or time in the UI. + * The first and only argument is a Date object representing the new date and time. + */ + onChange?(newDate: Date): void; + + /** + * Handler called when the user presses the confirm button + * Passes the current selected date + */ + onConfirm(date: Date): void; + + /** + * Handler called when the user presses the cancel button + */ + onCancel(): void; + + /** + * Called when the underlying modal finishes its' closing animation + * after Confirm was pressed. + */ + onHide?(date: Date): void; + + /** + * Used to locate this view in end-to-end tests. + */ + testID?: string; + + /** + * The style of the picker \ (iOS) + */ + pickerStyleIOS?: ViewStyle; +} + +type NativePickerProps = + | Omit<IOSNativeProps, "value" | "mode" | "onChange"> + | Omit<AndroidNativeProps, "value" | "mode" | "onChange">; + +export type ReactNativeModalDateTimePickerProps = DateTimePickerProps & + NativePickerProps; + +export default class DateTimePicker extends React.Component< + ReactNativeModalDateTimePickerProps, + any +> {} + +export const cancelButtonStyles: CancelButtonStylePropTypes; + +export const CancelButton: React.FunctionComponent<CancelButtonPropTypes>; + +export const confirmButtonStyles: ConfirmButtonStylePropTypes; + +export const ConfirmButton: React.FunctionComponent<ConfirmButtonPropTypes>; diff --git a/src/shared/libs/datePickerModal/index.js b/src/shared/libs/datePickerModal/index.js new file mode 100644 index 0000000..2c781a2 --- /dev/null +++ b/src/shared/libs/datePickerModal/index.js @@ -0,0 +1,5 @@ +import { DateTimePickerModal } from "./DateTimePickerModal"; + +export default DateTimePickerModal; + +export * from "./DateTimePickerModal"; diff --git a/src/shared/libs/datePickerModal/utils.js b/src/shared/libs/datePickerModal/utils.js new file mode 100644 index 0000000..aaa717f --- /dev/null +++ b/src/shared/libs/datePickerModal/utils.js @@ -0,0 +1,25 @@ +import { Dimensions, Platform } from "react-native"; + +export const isIphoneX = () => { + const { height, width } = Dimensions.get("window"); + + return ( + Platform.OS === "ios" && + !Platform.isPad && + !Platform.isTVOS && + (height === 780 || + width === 780 || + height === 812 || + width === 812 || + height === 844 || + width === 844 || + height === 852 || + width === 852 || + height === 896 || + width === 896 || + height === 926 || + width === 926 || + height === 932 || + width === 932) + ); +}; diff --git a/src/shared/libs/luxon/src/datetime.js b/src/shared/libs/luxon/src/datetime.js new file mode 100644 index 0000000..f98d183 --- /dev/null +++ b/src/shared/libs/luxon/src/datetime.js @@ -0,0 +1,2422 @@ +import Duration from "./duration.js"; +import Interval from "./interval.js"; +import Settings from "./settings.js"; +import Info from "./info.js"; +import Formatter from "./impl/formatter.js"; +import FixedOffsetZone from "./zones/fixedOffsetZone.js"; +import Locale from "./impl/locale.js"; +import { + isUndefined, + maybeArray, + isDate, + isNumber, + bestBy, + daysInMonth, + daysInYear, + isLeapYear, + weeksInWeekYear, + normalizeObject, + roundTo, + objToLocalTS, + padStart, +} from "./impl/util.js"; +import { normalizeZone } from "./impl/zoneUtil.js"; +import diff from "./impl/diff.js"; +import { parseRFC2822Date, parseISODate, parseHTTPDate, parseSQL } from "./impl/regexParser.js"; +import { + parseFromTokens, + explainFromTokens, + formatOptsToTokens, + expandMacroTokens, +} from "./impl/tokenParser.js"; +import { + gregorianToWeek, + weekToGregorian, + gregorianToOrdinal, + ordinalToGregorian, + hasInvalidGregorianData, + hasInvalidWeekData, + hasInvalidOrdinalData, + hasInvalidTimeData, + usesLocalWeekValues, + isoWeekdayToLocal, +} from "./impl/conversions.js"; +import * as Formats from "./impl/formats.js"; +import { + InvalidArgumentError, + ConflictingSpecificationError, + InvalidUnitError, + InvalidDateTimeError, +} from "./errors.js"; +import Invalid from "./impl/invalid.js"; + +const INVALID = "Invalid DateTime"; +const MAX_DATE = 8.64e15; + +function unsupportedZone(zone) { + return new Invalid("unsupported zone", `the zone "${zone.name}" is not supported`); +} + +// we cache week data on the DT object and this intermediates the cache +/** + * @param {DateTime} dt + */ +function possiblyCachedWeekData(dt) { + if (dt.weekData === null) { + dt.weekData = gregorianToWeek(dt.c); + } + return dt.weekData; +} + +/** + * @param {DateTime} dt + */ +function possiblyCachedLocalWeekData(dt) { + if (dt.localWeekData === null) { + dt.localWeekData = gregorianToWeek( + dt.c, + dt.loc.getMinDaysInFirstWeek(), + dt.loc.getStartOfWeek() + ); + } + return dt.localWeekData; +} + +// clone really means, "make a new object with these modifications". all "setters" really use this +// to create a new object while only changing some of the properties +function clone(inst, alts) { + const current = { + ts: inst.ts, + zone: inst.zone, + c: inst.c, + o: inst.o, + loc: inst.loc, + invalid: inst.invalid, + }; + return new DateTime({ ...current, ...alts, old: current }); +} + +// find the right offset a given local time. The o input is our guess, which determines which +// offset we'll pick in ambiguous cases (e.g. there are two 3 AMs b/c Fallback DST) +function fixOffset(localTS, o, tz) { + // Our UTC time is just a guess because our offset is just a guess + let utcGuess = localTS - o * 60 * 1000; + + // Test whether the zone matches the offset for this ts + const o2 = tz.offset(utcGuess); + + // If so, offset didn't change and we're done + if (o === o2) { + return [utcGuess, o]; + } + + // If not, change the ts by the difference in the offset + utcGuess -= (o2 - o) * 60 * 1000; + + // If that gives us the local time we want, we're done + const o3 = tz.offset(utcGuess); + if (o2 === o3) { + return [utcGuess, o2]; + } + + // If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time + return [localTS - Math.min(o2, o3) * 60 * 1000, Math.max(o2, o3)]; +} + +// convert an epoch timestamp into a calendar object with the given offset +function tsToObj(ts, offset) { + ts += offset * 60 * 1000; + + const d = new Date(ts); + + return { + year: d.getUTCFullYear(), + month: d.getUTCMonth() + 1, + day: d.getUTCDate(), + hour: d.getUTCHours(), + minute: d.getUTCMinutes(), + second: d.getUTCSeconds(), + millisecond: d.getUTCMilliseconds(), + }; +} + +// convert a calendar object to a epoch timestamp +function objToTS(obj, offset, zone) { + return fixOffset(objToLocalTS(obj), offset, zone); +} + +// create a new DT instance by adding a duration, adjusting for DSTs +function adjustTime(inst, dur) { + const oPre = inst.o, + year = inst.c.year + Math.trunc(dur.years), + month = inst.c.month + Math.trunc(dur.months) + Math.trunc(dur.quarters) * 3, + c = { + ...inst.c, + year, + month, + day: + Math.min(inst.c.day, daysInMonth(year, month)) + + Math.trunc(dur.days) + + Math.trunc(dur.weeks) * 7, + }, + millisToAdd = Duration.fromObject({ + years: dur.years - Math.trunc(dur.years), + quarters: dur.quarters - Math.trunc(dur.quarters), + months: dur.months - Math.trunc(dur.months), + weeks: dur.weeks - Math.trunc(dur.weeks), + days: dur.days - Math.trunc(dur.days), + hours: dur.hours, + minutes: dur.minutes, + seconds: dur.seconds, + milliseconds: dur.milliseconds, + }).as("milliseconds"), + localTS = objToLocalTS(c); + + let [ts, o] = fixOffset(localTS, oPre, inst.zone); + + if (millisToAdd !== 0) { + ts += millisToAdd; + // that could have changed the offset by going over a DST, but we want to keep the ts the same + o = inst.zone.offset(ts); + } + + return { ts, o }; +} + +// helper useful in turning the results of parsing into real dates +// by handling the zone options +function parseDataToDateTime(parsed, parsedZone, opts, format, text, specificOffset) { + const { setZone, zone } = opts; + if ((parsed && Object.keys(parsed).length !== 0) || parsedZone) { + const interpretationZone = parsedZone || zone, + inst = DateTime.fromObject(parsed, { + ...opts, + zone: interpretationZone, + specificOffset, + }); + return setZone ? inst : inst.setZone(zone); + } else { + return DateTime.invalid( + new Invalid("unparsable", `the input "${text}" can't be parsed as ${format}`) + ); + } +} + +// if you want to output a technical format (e.g. RFC 2822), this helper +// helps handle the details +function toTechFormat(dt, format, allowZ = true) { + return dt.isValid + ? Formatter.create(Locale.create("en-US"), { + allowZ, + forceSimple: true, + }).formatDateTimeFromString(dt, format) + : null; +} + +function toISODate(o, extended) { + const longFormat = o.c.year > 9999 || o.c.year < 0; + let c = ""; + if (longFormat && o.c.year >= 0) c += "+"; + c += padStart(o.c.year, longFormat ? 6 : 4); + + if (extended) { + c += "-"; + c += padStart(o.c.month); + c += "-"; + c += padStart(o.c.day); + } else { + c += padStart(o.c.month); + c += padStart(o.c.day); + } + return c; +} + +function toISOTime( + o, + extended, + suppressSeconds, + suppressMilliseconds, + includeOffset, + extendedZone +) { + let c = padStart(o.c.hour); + if (extended) { + c += ":"; + c += padStart(o.c.minute); + if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) { + c += ":"; + } + } else { + c += padStart(o.c.minute); + } + + if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) { + c += padStart(o.c.second); + + if (o.c.millisecond !== 0 || !suppressMilliseconds) { + c += "."; + c += padStart(o.c.millisecond, 3); + } + } + + if (includeOffset) { + if (o.isOffsetFixed && o.offset === 0 && !extendedZone) { + c += "Z"; + } else if (o.o < 0) { + c += "-"; + c += padStart(Math.trunc(-o.o / 60)); + c += ":"; + c += padStart(Math.trunc(-o.o % 60)); + } else { + c += "+"; + c += padStart(Math.trunc(o.o / 60)); + c += ":"; + c += padStart(Math.trunc(o.o % 60)); + } + } + + if (extendedZone) { + c += "[" + o.zone.ianaName + "]"; + } + return c; +} + +// defaults for unspecified units in the supported calendars +const defaultUnitValues = { + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }, + defaultWeekUnitValues = { + weekNumber: 1, + weekday: 1, + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }, + defaultOrdinalUnitValues = { + ordinal: 1, + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }; + +// Units in the supported calendars, sorted by bigness +const orderedUnits = ["year", "month", "day", "hour", "minute", "second", "millisecond"], + orderedWeekUnits = [ + "weekYear", + "weekNumber", + "weekday", + "hour", + "minute", + "second", + "millisecond", + ], + orderedOrdinalUnits = ["year", "ordinal", "hour", "minute", "second", "millisecond"]; + +// standardize case and plurality in units +function normalizeUnit(unit) { + const normalized = { + year: "year", + years: "year", + month: "month", + months: "month", + day: "day", + days: "day", + hour: "hour", + hours: "hour", + minute: "minute", + minutes: "minute", + quarter: "quarter", + quarters: "quarter", + second: "second", + seconds: "second", + millisecond: "millisecond", + milliseconds: "millisecond", + weekday: "weekday", + weekdays: "weekday", + weeknumber: "weekNumber", + weeksnumber: "weekNumber", + weeknumbers: "weekNumber", + weekyear: "weekYear", + weekyears: "weekYear", + ordinal: "ordinal", + }[unit.toLowerCase()]; + + if (!normalized) throw new InvalidUnitError(unit); + + return normalized; +} + +function normalizeUnitWithLocalWeeks(unit) { + switch (unit.toLowerCase()) { + case "localweekday": + case "localweekdays": + return "localWeekday"; + case "localweeknumber": + case "localweeknumbers": + return "localWeekNumber"; + case "localweekyear": + case "localweekyears": + return "localWeekYear"; + default: + return normalizeUnit(unit); + } +} + +// this is a dumbed down version of fromObject() that runs about 60% faster +// but doesn't do any validation, makes a bunch of assumptions about what units +// are present, and so on. +function quickDT(obj, opts) { + const zone = normalizeZone(opts.zone, Settings.defaultZone), + loc = Locale.fromObject(opts), + tsNow = Settings.now(); + + let ts, o; + + // assume we have the higher-order units + if (!isUndefined(obj.year)) { + for (const u of orderedUnits) { + if (isUndefined(obj[u])) { + obj[u] = defaultUnitValues[u]; + } + } + + const invalid = hasInvalidGregorianData(obj) || hasInvalidTimeData(obj); + if (invalid) { + return DateTime.invalid(invalid); + } + + const offsetProvis = zone.offset(tsNow); + [ts, o] = objToTS(obj, offsetProvis, zone); + } else { + ts = tsNow; + } + + return new DateTime({ ts, zone, loc, o }); +} + +function diffRelative(start, end, opts) { + const round = isUndefined(opts.round) ? true : opts.round, + format = (c, unit) => { + c = roundTo(c, round || opts.calendary ? 0 : 2, true); + const formatter = end.loc.clone(opts).relFormatter(opts); + return formatter.format(c, unit); + }, + differ = (unit) => { + if (opts.calendary) { + if (!end.hasSame(start, unit)) { + return end.startOf(unit).diff(start.startOf(unit), unit).get(unit); + } else return 0; + } else { + return end.diff(start, unit).get(unit); + } + }; + + if (opts.unit) { + return format(differ(opts.unit), opts.unit); + } + + for (const unit of opts.units) { + const count = differ(unit); + if (Math.abs(count) >= 1) { + return format(count, unit); + } + } + return format(start > end ? -0 : 0, opts.units[opts.units.length - 1]); +} + +function lastOpts(argList) { + let opts = {}, + args; + if (argList.length > 0 && typeof argList[argList.length - 1] === "object") { + opts = argList[argList.length - 1]; + args = Array.from(argList).slice(0, argList.length - 1); + } else { + args = Array.from(argList); + } + return [opts, args]; +} + +/** + * A DateTime is an immutable data structure representing a specific date and time and accompanying methods. It contains class and instance methods for creating, parsing, interrogating, transforming, and formatting them. + * + * A DateTime comprises of: + * * A timestamp. Each DateTime instance refers to a specific millisecond of the Unix epoch. + * * A time zone. Each instance is considered in the context of a specific zone (by default the local system's zone). + * * Configuration properties that effect how output strings are formatted, such as `locale`, `numberingSystem`, and `outputCalendar`. + * + * Here is a brief overview of the most commonly used functionality it provides: + * + * * **Creation**: To create a DateTime from its components, use one of its factory class methods: {@link DateTime.local}, {@link DateTime.utc}, and (most flexibly) {@link DateTime.fromObject}. To create one from a standard string format, use {@link DateTime.fromISO}, {@link DateTime.fromHTTP}, and {@link DateTime.fromRFC2822}. To create one from a custom string format, use {@link DateTime.fromFormat}. To create one from a native JS date, use {@link DateTime.fromJSDate}. + * * **Gregorian calendar and time**: To examine the Gregorian properties of a DateTime individually (i.e as opposed to collectively through {@link DateTime#toObject}), use the {@link DateTime#year}, {@link DateTime#month}, + * {@link DateTime#day}, {@link DateTime#hour}, {@link DateTime#minute}, {@link DateTime#second}, {@link DateTime#millisecond} accessors. + * * **Week calendar**: For ISO week calendar attributes, see the {@link DateTime#weekYear}, {@link DateTime#weekNumber}, and {@link DateTime#weekday} accessors. + * * **Configuration** See the {@link DateTime#locale} and {@link DateTime#numberingSystem} accessors. + * * **Transformation**: To transform the DateTime into other DateTimes, use {@link DateTime#set}, {@link DateTime#reconfigure}, {@link DateTime#setZone}, {@link DateTime#setLocale}, {@link DateTime.plus}, {@link DateTime#minus}, {@link DateTime#endOf}, {@link DateTime#startOf}, {@link DateTime#toUTC}, and {@link DateTime#toLocal}. + * * **Output**: To convert the DateTime to other representations, use the {@link DateTime#toRelative}, {@link DateTime#toRelativeCalendar}, {@link DateTime#toJSON}, {@link DateTime#toISO}, {@link DateTime#toHTTP}, {@link DateTime#toObject}, {@link DateTime#toRFC2822}, {@link DateTime#toString}, {@link DateTime#toLocaleString}, {@link DateTime#toFormat}, {@link DateTime#toMillis} and {@link DateTime#toJSDate}. + * + * There's plenty others documented below. In addition, for more information on subtler topics like internationalization, time zones, alternative calendars, validity, and so on, see the external documentation. + */ +export default class DateTime { + /** + * @access private + */ + constructor(config) { + const zone = config.zone || Settings.defaultZone; + + let invalid = + config.invalid || + (Number.isNaN(config.ts) ? new Invalid("invalid input") : null) || + (!zone.isValid ? unsupportedZone(zone) : null); + /** + * @access private + */ + this.ts = isUndefined(config.ts) ? Settings.now() : config.ts; + + let c = null, + o = null; + if (!invalid) { + const unchanged = config.old && config.old.ts === this.ts && config.old.zone.equals(zone); + + if (unchanged) { + [c, o] = [config.old.c, config.old.o]; + } else { + const ot = zone.offset(this.ts); + c = tsToObj(this.ts, ot); + invalid = Number.isNaN(c.year) ? new Invalid("invalid input") : null; + c = invalid ? null : c; + o = invalid ? null : ot; + } + } + + /** + * @access private + */ + this._zone = zone; + /** + * @access private + */ + this.loc = config.loc || Locale.create(); + /** + * @access private + */ + this.invalid = invalid; + /** + * @access private + */ + this.weekData = null; + /** + * @access private + */ + this.localWeekData = null; + /** + * @access private + */ + this.c = c; + /** + * @access private + */ + this.o = o; + /** + * @access private + */ + this.isLuxonDateTime = true; + } + + // CONSTRUCT + + /** + * Create a DateTime for the current instant, in the system's time zone. + * + * Use Settings to override these default values if needed. + * @example DateTime.now().toISO() //~> now in the ISO format + * @return {DateTime} + */ + static now() { + return new DateTime({}); + } + + /** + * Create a local DateTime + * @param {number} [year] - The calendar year. If omitted (as in, call `local()` with no arguments), the current time will be used + * @param {number} [month=1] - The month, 1-indexed + * @param {number} [day=1] - The day of the month, 1-indexed + * @param {number} [hour=0] - The hour of the day, in 24-hour time + * @param {number} [minute=0] - The minute of the hour, meaning a number between 0 and 59 + * @param {number} [second=0] - The second of the minute, meaning a number between 0 and 59 + * @param {number} [millisecond=0] - The millisecond of the second, meaning a number between 0 and 999 + * @example DateTime.local() //~> now + * @example DateTime.local({ zone: "America/New_York" }) //~> now, in US east coast time + * @example DateTime.local(2017) //~> 2017-01-01T00:00:00 + * @example DateTime.local(2017, 3) //~> 2017-03-01T00:00:00 + * @example DateTime.local(2017, 3, 12, { locale: "fr" }) //~> 2017-03-12T00:00:00, with a French locale + * @example DateTime.local(2017, 3, 12, 5) //~> 2017-03-12T05:00:00 + * @example DateTime.local(2017, 3, 12, 5, { zone: "utc" }) //~> 2017-03-12T05:00:00, in UTC + * @example DateTime.local(2017, 3, 12, 5, 45) //~> 2017-03-12T05:45:00 + * @example DateTime.local(2017, 3, 12, 5, 45, 10) //~> 2017-03-12T05:45:10 + * @example DateTime.local(2017, 3, 12, 5, 45, 10, 765) //~> 2017-03-12T05:45:10.765 + * @return {DateTime} + */ + static local() { + const [opts, args] = lastOpts(arguments), + [year, month, day, hour, minute, second, millisecond] = args; + return quickDT({ year, month, day, hour, minute, second, millisecond }, opts); + } + + /** + * Create a DateTime in UTC + * @param {number} [year] - The calendar year. If omitted (as in, call `utc()` with no arguments), the current time will be used + * @param {number} [month=1] - The month, 1-indexed + * @param {number} [day=1] - The day of the month + * @param {number} [hour=0] - The hour of the day, in 24-hour time + * @param {number} [minute=0] - The minute of the hour, meaning a number between 0 and 59 + * @param {number} [second=0] - The second of the minute, meaning a number between 0 and 59 + * @param {number} [millisecond=0] - The millisecond of the second, meaning a number between 0 and 999 + * @param {Object} options - configuration options for the DateTime + * @param {string} [options.locale] - a locale to set on the resulting DateTime instance + * @param {string} [options.outputCalendar] - the output calendar to set on the resulting DateTime instance + * @param {string} [options.numberingSystem] - the numbering system to set on the resulting DateTime instance + * @example DateTime.utc() //~> now + * @example DateTime.utc(2017) //~> 2017-01-01T00:00:00Z + * @example DateTime.utc(2017, 3) //~> 2017-03-01T00:00:00Z + * @example DateTime.utc(2017, 3, 12) //~> 2017-03-12T00:00:00Z + * @example DateTime.utc(2017, 3, 12, 5) //~> 2017-03-12T05:00:00Z + * @example DateTime.utc(2017, 3, 12, 5, 45) //~> 2017-03-12T05:45:00Z + * @example DateTime.utc(2017, 3, 12, 5, 45, { locale: "fr" }) //~> 2017-03-12T05:45:00Z with a French locale + * @example DateTime.utc(2017, 3, 12, 5, 45, 10) //~> 2017-03-12T05:45:10Z + * @example DateTime.utc(2017, 3, 12, 5, 45, 10, 765, { locale: "fr" }) //~> 2017-03-12T05:45:10.765Z with a French locale + * @return {DateTime} + */ + static utc() { + const [opts, args] = lastOpts(arguments), + [year, month, day, hour, minute, second, millisecond] = args; + + opts.zone = FixedOffsetZone.utcInstance; + return quickDT({ year, month, day, hour, minute, second, millisecond }, opts); + } + + /** + * Create a DateTime from a JavaScript Date object. Uses the default zone. + * @param {Date} date - a JavaScript Date object + * @param {Object} options - configuration options for the DateTime + * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into + * @return {DateTime} + */ + static fromJSDate(date, options = {}) { + const ts = isDate(date) ? date.valueOf() : NaN; + if (Number.isNaN(ts)) { + return DateTime.invalid("invalid input"); + } + + const zoneToUse = normalizeZone(options.zone, Settings.defaultZone); + if (!zoneToUse.isValid) { + return DateTime.invalid(unsupportedZone(zoneToUse)); + } + + return new DateTime({ + ts: ts, + zone: zoneToUse, + loc: Locale.fromObject(options), + }); + } + + /** + * Create a DateTime from a number of milliseconds since the epoch (meaning since 1 January 1970 00:00:00 UTC). Uses the default zone. + * @param {number} milliseconds - a number of milliseconds since 1970 UTC + * @param {Object} options - configuration options for the DateTime + * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into + * @param {string} [options.locale] - a locale to set on the resulting DateTime instance + * @param {string} options.outputCalendar - the output calendar to set on the resulting DateTime instance + * @param {string} options.numberingSystem - the numbering system to set on the resulting DateTime instance + * @return {DateTime} + */ + static fromMillis(milliseconds, options = {}) { + if (!isNumber(milliseconds)) { + throw new InvalidArgumentError( + `fromMillis requires a numerical input, but received a ${typeof milliseconds} with value ${milliseconds}` + ); + } else if (milliseconds < -MAX_DATE || milliseconds > MAX_DATE) { + // this isn't perfect because because we can still end up out of range because of additional shifting, but it's a start + return DateTime.invalid("Timestamp out of range"); + } else { + return new DateTime({ + ts: milliseconds, + zone: normalizeZone(options.zone, Settings.defaultZone), + loc: Locale.fromObject(options), + }); + } + } + + /** + * Create a DateTime from a number of seconds since the epoch (meaning since 1 January 1970 00:00:00 UTC). Uses the default zone. + * @param {number} seconds - a number of seconds since 1970 UTC + * @param {Object} options - configuration options for the DateTime + * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into + * @param {string} [options.locale] - a locale to set on the resulting DateTime instance + * @param {string} options.outputCalendar - the output calendar to set on the resulting DateTime instance + * @param {string} options.numberingSystem - the numbering system to set on the resulting DateTime instance + * @return {DateTime} + */ + static fromSeconds(seconds, options = {}) { + if (!isNumber(seconds)) { + throw new InvalidArgumentError("fromSeconds requires a numerical input"); + } else { + return new DateTime({ + ts: seconds * 1000, + zone: normalizeZone(options.zone, Settings.defaultZone), + loc: Locale.fromObject(options), + }); + } + } + + /** + * Create a DateTime from a JavaScript object with keys like 'year' and 'hour' with reasonable defaults. + * @param {Object} obj - the object to create the DateTime from + * @param {number} obj.year - a year, such as 1987 + * @param {number} obj.month - a month, 1-12 + * @param {number} obj.day - a day of the month, 1-31, depending on the month + * @param {number} obj.ordinal - day of the year, 1-365 or 366 + * @param {number} obj.weekYear - an ISO week year + * @param {number} obj.weekNumber - an ISO week number, between 1 and 52 or 53, depending on the year + * @param {number} obj.weekday - an ISO weekday, 1-7, where 1 is Monday and 7 is Sunday + * @param {number} obj.localWeekYear - a week year, according to the locale + * @param {number} obj.localWeekNumber - a week number, between 1 and 52 or 53, depending on the year, according to the locale + * @param {number} obj.localWeekday - a weekday, 1-7, where 1 is the first and 7 is the last day of the week, according to the locale + * @param {number} obj.hour - hour of the day, 0-23 + * @param {number} obj.minute - minute of the hour, 0-59 + * @param {number} obj.second - second of the minute, 0-59 + * @param {number} obj.millisecond - millisecond of the second, 0-999 + * @param {Object} opts - options for creating this DateTime + * @param {string|Zone} [opts.zone='local'] - interpret the numbers in the context of a particular zone. Can take any value taken as the first argument to setZone() + * @param {string} [opts.locale='system\'s locale'] - a locale to set on the resulting DateTime instance + * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance + * @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance + * @example DateTime.fromObject({ year: 1982, month: 5, day: 25}).toISODate() //=> '1982-05-25' + * @example DateTime.fromObject({ year: 1982 }).toISODate() //=> '1982-01-01' + * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }) //~> today at 10:26:06 + * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }, { zone: 'utc' }), + * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }, { zone: 'local' }) + * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }, { zone: 'America/New_York' }) + * @example DateTime.fromObject({ weekYear: 2016, weekNumber: 2, weekday: 3 }).toISODate() //=> '2016-01-13' + * @example DateTime.fromObject({ localWeekYear: 2022, localWeekNumber: 1, localWeekday: 1 }, { locale: "en-US" }).toISODate() //=> '2021-12-26' + * @return {DateTime} + */ + static fromObject(obj, opts = {}) { + obj = obj || {}; + const zoneToUse = normalizeZone(opts.zone, Settings.defaultZone); + if (!zoneToUse.isValid) { + return DateTime.invalid(unsupportedZone(zoneToUse)); + } + + const loc = Locale.fromObject(opts); + const normalized = normalizeObject(obj, normalizeUnitWithLocalWeeks); + const { minDaysInFirstWeek, startOfWeek } = usesLocalWeekValues(normalized, loc); + + const tsNow = Settings.now(), + offsetProvis = !isUndefined(opts.specificOffset) + ? opts.specificOffset + : zoneToUse.offset(tsNow), + containsOrdinal = !isUndefined(normalized.ordinal), + containsGregorYear = !isUndefined(normalized.year), + containsGregorMD = !isUndefined(normalized.month) || !isUndefined(normalized.day), + containsGregor = containsGregorYear || containsGregorMD, + definiteWeekDef = normalized.weekYear || normalized.weekNumber; + + // cases: + // just a weekday -> this week's instance of that weekday, no worries + // (gregorian data or ordinal) + (weekYear or weekNumber) -> error + // (gregorian month or day) + ordinal -> error + // otherwise just use weeks or ordinals or gregorian, depending on what's specified + + if ((containsGregor || containsOrdinal) && definiteWeekDef) { + throw new ConflictingSpecificationError( + "Can't mix weekYear/weekNumber units with year/month/day or ordinals" + ); + } + + if (containsGregorMD && containsOrdinal) { + throw new ConflictingSpecificationError("Can't mix ordinal dates with month/day"); + } + + const useWeekData = definiteWeekDef || (normalized.weekday && !containsGregor); + + // configure ourselves to deal with gregorian dates or week stuff + let units, + defaultValues, + objNow = tsToObj(tsNow, offsetProvis); + if (useWeekData) { + units = orderedWeekUnits; + defaultValues = defaultWeekUnitValues; + objNow = gregorianToWeek(objNow, minDaysInFirstWeek, startOfWeek); + } else if (containsOrdinal) { + units = orderedOrdinalUnits; + defaultValues = defaultOrdinalUnitValues; + objNow = gregorianToOrdinal(objNow); + } else { + units = orderedUnits; + defaultValues = defaultUnitValues; + } + + // set default values for missing stuff + let foundFirst = false; + for (const u of units) { + const v = normalized[u]; + if (!isUndefined(v)) { + foundFirst = true; + } else if (foundFirst) { + normalized[u] = defaultValues[u]; + } else { + normalized[u] = objNow[u]; + } + } + + // make sure the values we have are in range + const higherOrderInvalid = useWeekData + ? hasInvalidWeekData(normalized, minDaysInFirstWeek, startOfWeek) + : containsOrdinal + ? hasInvalidOrdinalData(normalized) + : hasInvalidGregorianData(normalized), + invalid = higherOrderInvalid || hasInvalidTimeData(normalized); + + if (invalid) { + return DateTime.invalid(invalid); + } + + // compute the actual time + const gregorian = useWeekData + ? weekToGregorian(normalized, minDaysInFirstWeek, startOfWeek) + : containsOrdinal + ? ordinalToGregorian(normalized) + : normalized, + [tsFinal, offsetFinal] = objToTS(gregorian, offsetProvis, zoneToUse), + inst = new DateTime({ + ts: tsFinal, + zone: zoneToUse, + o: offsetFinal, + loc, + }); + + // gregorian data + weekday serves only to validate + if (normalized.weekday && containsGregor && obj.weekday !== inst.weekday) { + return DateTime.invalid( + "mismatched weekday", + `you can't specify both a weekday of ${normalized.weekday} and a date of ${inst.toISO()}` + ); + } + + return inst; + } + + /** + * Create a DateTime from an ISO 8601 string + * @param {string} text - the ISO string + * @param {Object} opts - options to affect the creation + * @param {string|Zone} [opts.zone='local'] - use this zone if no offset is specified in the input string itself. Will also convert the time to this zone + * @param {boolean} [opts.setZone=false] - override the zone with a fixed-offset zone specified in the string itself, if it specifies one + * @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance + * @param {string} [opts.outputCalendar] - the output calendar to set on the resulting DateTime instance + * @param {string} [opts.numberingSystem] - the numbering system to set on the resulting DateTime instance + * @example DateTime.fromISO('2016-05-25T09:08:34.123') + * @example DateTime.fromISO('2016-05-25T09:08:34.123+06:00') + * @example DateTime.fromISO('2016-05-25T09:08:34.123+06:00', {setZone: true}) + * @example DateTime.fromISO('2016-05-25T09:08:34.123', {zone: 'utc'}) + * @example DateTime.fromISO('2016-W05-4') + * @return {DateTime} + */ + static fromISO(text, opts = {}) { + const [vals, parsedZone] = parseISODate(text); + return parseDataToDateTime(vals, parsedZone, opts, "ISO 8601", text); + } + + /** + * Create a DateTime from an RFC 2822 string + * @param {string} text - the RFC 2822 string + * @param {Object} opts - options to affect the creation + * @param {string|Zone} [opts.zone='local'] - convert the time to this zone. Since the offset is always specified in the string itself, this has no effect on the interpretation of string, merely the zone the resulting DateTime is expressed in. + * @param {boolean} [opts.setZone=false] - override the zone with a fixed-offset zone specified in the string itself, if it specifies one + * @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance + * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance + * @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance + * @example DateTime.fromRFC2822('25 Nov 2016 13:23:12 GMT') + * @example DateTime.fromRFC2822('Fri, 25 Nov 2016 13:23:12 +0600') + * @example DateTime.fromRFC2822('25 Nov 2016 13:23 Z') + * @return {DateTime} + */ + static fromRFC2822(text, opts = {}) { + const [vals, parsedZone] = parseRFC2822Date(text); + return parseDataToDateTime(vals, parsedZone, opts, "RFC 2822", text); + } + + /** + * Create a DateTime from an HTTP header date + * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1 + * @param {string} text - the HTTP header date + * @param {Object} opts - options to affect the creation + * @param {string|Zone} [opts.zone='local'] - convert the time to this zone. Since HTTP dates are always in UTC, this has no effect on the interpretation of string, merely the zone the resulting DateTime is expressed in. + * @param {boolean} [opts.setZone=false] - override the zone with the fixed-offset zone specified in the string. For HTTP dates, this is always UTC, so this option is equivalent to setting the `zone` option to 'utc', but this option is included for consistency with similar methods. + * @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance + * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance + * @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance + * @example DateTime.fromHTTP('Sun, 06 Nov 1994 08:49:37 GMT') + * @example DateTime.fromHTTP('Sunday, 06-Nov-94 08:49:37 GMT') + * @example DateTime.fromHTTP('Sun Nov 6 08:49:37 1994') + * @return {DateTime} + */ + static fromHTTP(text, opts = {}) { + const [vals, parsedZone] = parseHTTPDate(text); + return parseDataToDateTime(vals, parsedZone, opts, "HTTP", opts); + } + + /** + * Create a DateTime from an input string and format string. + * Defaults to en-US if no locale has been specified, regardless of the system's locale. For a table of tokens and their interpretations, see [here](https://moment.github.io/luxon/#/parsing?id=table-of-tokens). + * @param {string} text - the string to parse + * @param {string} fmt - the format the string is expected to be in (see the link below for the formats) + * @param {Object} opts - options to affect the creation + * @param {string|Zone} [opts.zone='local'] - use this zone if no offset is specified in the input string itself. Will also convert the DateTime to this zone + * @param {boolean} [opts.setZone=false] - override the zone with a zone specified in the string itself, if it specifies one + * @param {string} [opts.locale='en-US'] - a locale string to use when parsing. Will also set the DateTime to this locale + * @param {string} opts.numberingSystem - the numbering system to use when parsing. Will also set the resulting DateTime to this numbering system + * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance + * @return {DateTime} + */ + static fromFormat(text, fmt, opts = {}) { + if (isUndefined(text) || isUndefined(fmt)) { + throw new InvalidArgumentError("fromFormat requires an input string and a format"); + } + + const { locale = null, numberingSystem = null } = opts, + localeToUse = Locale.fromOpts({ + locale, + numberingSystem, + defaultToEN: true, + }), + [vals, parsedZone, specificOffset, invalid] = parseFromTokens(localeToUse, text, fmt); + if (invalid) { + return DateTime.invalid(invalid); + } else { + return parseDataToDateTime(vals, parsedZone, opts, `format ${fmt}`, text, specificOffset); + } + } + + /** + * @deprecated use fromFormat instead + */ + static fromString(text, fmt, opts = {}) { + return DateTime.fromFormat(text, fmt, opts); + } + + /** + * Create a DateTime from a SQL date, time, or datetime + * Defaults to en-US if no locale has been specified, regardless of the system's locale + * @param {string} text - the string to parse + * @param {Object} opts - options to affect the creation + * @param {string|Zone} [opts.zone='local'] - use this zone if no offset is specified in the input string itself. Will also convert the DateTime to this zone + * @param {boolean} [opts.setZone=false] - override the zone with a zone specified in the string itself, if it specifies one + * @param {string} [opts.locale='en-US'] - a locale string to use when parsing. Will also set the DateTime to this locale + * @param {string} opts.numberingSystem - the numbering system to use when parsing. Will also set the resulting DateTime to this numbering system + * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance + * @example DateTime.fromSQL('2017-05-15') + * @example DateTime.fromSQL('2017-05-15 09:12:34') + * @example DateTime.fromSQL('2017-05-15 09:12:34.342') + * @example DateTime.fromSQL('2017-05-15 09:12:34.342+06:00') + * @example DateTime.fromSQL('2017-05-15 09:12:34.342 America/Los_Angeles') + * @example DateTime.fromSQL('2017-05-15 09:12:34.342 America/Los_Angeles', { setZone: true }) + * @example DateTime.fromSQL('2017-05-15 09:12:34.342', { zone: 'America/Los_Angeles' }) + * @example DateTime.fromSQL('09:12:34.342') + * @return {DateTime} + */ + static fromSQL(text, opts = {}) { + const [vals, parsedZone] = parseSQL(text); + return parseDataToDateTime(vals, parsedZone, opts, "SQL", text); + } + + /** + * Create an invalid DateTime. + * @param {string} reason - simple string of why this DateTime is invalid. Should not contain parameters or anything else data-dependent. + * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information + * @return {DateTime} + */ + static invalid(reason, explanation = null) { + if (!reason) { + throw new InvalidArgumentError("need to specify a reason the DateTime is invalid"); + } + + const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation); + + if (Settings.throwOnInvalid) { + throw new InvalidDateTimeError(invalid); + } else { + return new DateTime({ invalid }); + } + } + + /** + * Check if an object is an instance of DateTime. Works across context boundaries + * @param {object} o + * @return {boolean} + */ + static isDateTime(o) { + return (o && o.isLuxonDateTime) || false; + } + + /** + * Produce the format string for a set of options + * @param formatOpts + * @param localeOpts + * @returns {string} + */ + static parseFormatForOpts(formatOpts, localeOpts = {}) { + const tokenList = formatOptsToTokens(formatOpts, Locale.fromObject(localeOpts)); + return !tokenList ? null : tokenList.map((t) => (t ? t.val : null)).join(""); + } + + /** + * Produce the the fully expanded format token for the locale + * Does NOT quote characters, so quoted tokens will not round trip correctly + * @param fmt + * @param localeOpts + * @returns {string} + */ + static expandFormat(fmt, localeOpts = {}) { + const expanded = expandMacroTokens(Formatter.parseFormat(fmt), Locale.fromObject(localeOpts)); + return expanded.map((t) => t.val).join(""); + } + + // INFO + + /** + * Get the value of unit. + * @param {string} unit - a unit such as 'minute' or 'day' + * @example DateTime.local(2017, 7, 4).get('month'); //=> 7 + * @example DateTime.local(2017, 7, 4).get('day'); //=> 4 + * @return {number} + */ + get(unit) { + return this[unit]; + } + + /** + * Returns whether the DateTime is valid. Invalid DateTimes occur when: + * * The DateTime was created from invalid calendar information, such as the 13th month or February 30 + * * The DateTime was created by an operation on another invalid date + * @type {boolean} + */ + get isValid() { + return this.invalid === null; + } + + /** + * Returns an error code if this DateTime is invalid, or null if the DateTime is valid + * @type {string} + */ + get invalidReason() { + return this.invalid ? this.invalid.reason : null; + } + + /** + * Returns an explanation of why this DateTime became invalid, or null if the DateTime is valid + * @type {string} + */ + get invalidExplanation() { + return this.invalid ? this.invalid.explanation : null; + } + + /** + * Get the locale of a DateTime, such 'en-GB'. The locale is used when formatting the DateTime + * + * @type {string} + */ + get locale() { + return this.isValid ? this.loc.locale : null; + } + + /** + * Get the numbering system of a DateTime, such 'beng'. The numbering system is used when formatting the DateTime + * + * @type {string} + */ + get numberingSystem() { + return this.isValid ? this.loc.numberingSystem : null; + } + + /** + * Get the output calendar of a DateTime, such 'islamic'. The output calendar is used when formatting the DateTime + * + * @type {string} + */ + get outputCalendar() { + return this.isValid ? this.loc.outputCalendar : null; + } + + /** + * Get the time zone associated with this DateTime. + * @type {Zone} + */ + get zone() { + return this._zone; + } + + /** + * Get the name of the time zone. + * @type {string} + */ + get zoneName() { + return this.isValid ? this.zone.name : null; + } + + /** + * Get the year + * @example DateTime.local(2017, 5, 25).year //=> 2017 + * @type {number} + */ + get year() { + return this.isValid ? this.c.year : NaN; + } + + /** + * Get the quarter + * @example DateTime.local(2017, 5, 25).quarter //=> 2 + * @type {number} + */ + get quarter() { + return this.isValid ? Math.ceil(this.c.month / 3) : NaN; + } + + /** + * Get the month (1-12). + * @example DateTime.local(2017, 5, 25).month //=> 5 + * @type {number} + */ + get month() { + return this.isValid ? this.c.month : NaN; + } + + /** + * Get the day of the month (1-30ish). + * @example DateTime.local(2017, 5, 25).day //=> 25 + * @type {number} + */ + get day() { + return this.isValid ? this.c.day : NaN; + } + + /** + * Get the hour of the day (0-23). + * @example DateTime.local(2017, 5, 25, 9).hour //=> 9 + * @type {number} + */ + get hour() { + return this.isValid ? this.c.hour : NaN; + } + + /** + * Get the minute of the hour (0-59). + * @example DateTime.local(2017, 5, 25, 9, 30).minute //=> 30 + * @type {number} + */ + get minute() { + return this.isValid ? this.c.minute : NaN; + } + + /** + * Get the second of the minute (0-59). + * @example DateTime.local(2017, 5, 25, 9, 30, 52).second //=> 52 + * @type {number} + */ + get second() { + return this.isValid ? this.c.second : NaN; + } + + /** + * Get the millisecond of the second (0-999). + * @example DateTime.local(2017, 5, 25, 9, 30, 52, 654).millisecond //=> 654 + * @type {number} + */ + get millisecond() { + return this.isValid ? this.c.millisecond : NaN; + } + + /** + * Get the week year + * @see https://en.wikipedia.org/wiki/ISO_week_date + * @example DateTime.local(2014, 12, 31).weekYear //=> 2015 + * @type {number} + */ + get weekYear() { + return this.isValid ? possiblyCachedWeekData(this).weekYear : NaN; + } + + /** + * Get the week number of the week year (1-52ish). + * @see https://en.wikipedia.org/wiki/ISO_week_date + * @example DateTime.local(2017, 5, 25).weekNumber //=> 21 + * @type {number} + */ + get weekNumber() { + return this.isValid ? possiblyCachedWeekData(this).weekNumber : NaN; + } + + /** + * Get the day of the week. + * 1 is Monday and 7 is Sunday + * @see https://en.wikipedia.org/wiki/ISO_week_date + * @example DateTime.local(2014, 11, 31).weekday //=> 4 + * @type {number} + */ + get weekday() { + return this.isValid ? possiblyCachedWeekData(this).weekday : NaN; + } + + /** + * Returns true if this date is on a weekend according to the locale, false otherwise + * @returns {boolean} + */ + get isWeekend() { + return this.isValid && this.loc.getWeekendDays().includes(this.weekday); + } + + /** + * Get the day of the week according to the locale. + * 1 is the first day of the week and 7 is the last day of the week. + * If the locale assigns Sunday as the first day of the week, then a date which is a Sunday will return 1, + * @returns {number} + */ + get localWeekday() { + return this.isValid ? possiblyCachedLocalWeekData(this).weekday : NaN; + } + + /** + * Get the week number of the week year according to the locale. Different locales assign week numbers differently, + * because the week can start on different days of the week (see localWeekday) and because a different number of days + * is required for a week to count as the first week of a year. + * @returns {number} + */ + get localWeekNumber() { + return this.isValid ? possiblyCachedLocalWeekData(this).weekNumber : NaN; + } + + /** + * Get the week year according to the locale. Different locales assign week numbers (and therefor week years) + * differently, see localWeekNumber. + * @returns {number} + */ + get localWeekYear() { + return this.isValid ? possiblyCachedLocalWeekData(this).weekYear : NaN; + } + + /** + * Get the ordinal (meaning the day of the year) + * @example DateTime.local(2017, 5, 25).ordinal //=> 145 + * @type {number|DateTime} + */ + get ordinal() { + return this.isValid ? gregorianToOrdinal(this.c).ordinal : NaN; + } + + /** + * Get the human readable short month name, such as 'Oct'. + * Defaults to the system's locale if no locale has been specified + * @example DateTime.local(2017, 10, 30).monthShort //=> Oct + * @type {string} + */ + get monthShort() { + return this.isValid ? Info.months("short", { locObj: this.loc })[this.month - 1] : null; + } + + /** + * Get the human readable long month name, such as 'October'. + * Defaults to the system's locale if no locale has been specified + * @example DateTime.local(2017, 10, 30).monthLong //=> October + * @type {string} + */ + get monthLong() { + return this.isValid ? Info.months("long", { locObj: this.loc })[this.month - 1] : null; + } + + /** + * Get the human readable short weekday, such as 'Mon'. + * Defaults to the system's locale if no locale has been specified + * @example DateTime.local(2017, 10, 30).weekdayShort //=> Mon + * @type {string} + */ + get weekdayShort() { + return this.isValid ? Info.weekdays("short", { locObj: this.loc })[this.weekday - 1] : null; + } + + /** + * Get the human readable long weekday, such as 'Monday'. + * Defaults to the system's locale if no locale has been specified + * @example DateTime.local(2017, 10, 30).weekdayLong //=> Monday + * @type {string} + */ + get weekdayLong() { + return this.isValid ? Info.weekdays("long", { locObj: this.loc })[this.weekday - 1] : null; + } + + /** + * Get the UTC offset of this DateTime in minutes + * @example DateTime.now().offset //=> -240 + * @example DateTime.utc().offset //=> 0 + * @type {number} + */ + get offset() { + return this.isValid ? +this.o : NaN; + } + + /** + * Get the short human name for the zone's current offset, for example "EST" or "EDT". + * Defaults to the system's locale if no locale has been specified + * @type {string} + */ + get offsetNameShort() { + if (this.isValid) { + return this.zone.offsetName(this.ts, { + format: "short", + locale: this.locale, + }); + } else { + return null; + } + } + + /** + * Get the long human name for the zone's current offset, for example "Eastern Standard Time" or "Eastern Daylight Time". + * Defaults to the system's locale if no locale has been specified + * @type {string} + */ + get offsetNameLong() { + if (this.isValid) { + return this.zone.offsetName(this.ts, { + format: "long", + locale: this.locale, + }); + } else { + return null; + } + } + + /** + * Get whether this zone's offset ever changes, as in a DST. + * @type {boolean} + */ + get isOffsetFixed() { + return this.isValid ? this.zone.isUniversal : null; + } + + /** + * Get whether the DateTime is in a DST. + * @type {boolean} + */ + get isInDST() { + if (this.isOffsetFixed) { + return false; + } else { + return ( + this.offset > this.set({ month: 1, day: 1 }).offset || + this.offset > this.set({ month: 5 }).offset + ); + } + } + + /** + * Get those DateTimes which have the same local time as this DateTime, but a different offset from UTC + * in this DateTime's zone. During DST changes local time can be ambiguous, for example + * `2023-10-29T02:30:00` in `Europe/Berlin` can have offset `+01:00` or `+02:00`. + * This method will return both possible DateTimes if this DateTime's local time is ambiguous. + * @returns {DateTime[]} + */ + getPossibleOffsets() { + if (!this.isValid || this.isOffsetFixed) { + return [this]; + } + const dayMs = 86400000; + const minuteMs = 60000; + const localTS = objToLocalTS(this.c); + const oEarlier = this.zone.offset(localTS - dayMs); + const oLater = this.zone.offset(localTS + dayMs); + + const o1 = this.zone.offset(localTS - oEarlier * minuteMs); + const o2 = this.zone.offset(localTS - oLater * minuteMs); + if (o1 === o2) { + return [this]; + } + const ts1 = localTS - o1 * minuteMs; + const ts2 = localTS - o2 * minuteMs; + const c1 = tsToObj(ts1, o1); + const c2 = tsToObj(ts2, o2); + if ( + c1.hour === c2.hour && + c1.minute === c2.minute && + c1.second === c2.second && + c1.millisecond === c2.millisecond + ) { + return [clone(this, { ts: ts1 }), clone(this, { ts: ts2 })]; + } + return [this]; + } + + /** + * Returns true if this DateTime is in a leap year, false otherwise + * @example DateTime.local(2016).isInLeapYear //=> true + * @example DateTime.local(2013).isInLeapYear //=> false + * @type {boolean} + */ + get isInLeapYear() { + return isLeapYear(this.year); + } + + /** + * Returns the number of days in this DateTime's month + * @example DateTime.local(2016, 2).daysInMonth //=> 29 + * @example DateTime.local(2016, 3).daysInMonth //=> 31 + * @type {number} + */ + get daysInMonth() { + return daysInMonth(this.year, this.month); + } + + /** + * Returns the number of days in this DateTime's year + * @example DateTime.local(2016).daysInYear //=> 366 + * @example DateTime.local(2013).daysInYear //=> 365 + * @type {number} + */ + get daysInYear() { + return this.isValid ? daysInYear(this.year) : NaN; + } + + /** + * Returns the number of weeks in this DateTime's year + * @see https://en.wikipedia.org/wiki/ISO_week_date + * @example DateTime.local(2004).weeksInWeekYear //=> 53 + * @example DateTime.local(2013).weeksInWeekYear //=> 52 + * @type {number} + */ + get weeksInWeekYear() { + return this.isValid ? weeksInWeekYear(this.weekYear) : NaN; + } + + /** + * Returns the number of weeks in this DateTime's local week year + * @example DateTime.local(2020, 6, {locale: 'en-US'}).weeksInLocalWeekYear //=> 52 + * @example DateTime.local(2020, 6, {locale: 'de-DE'}).weeksInLocalWeekYear //=> 53 + * @type {number} + */ + get weeksInLocalWeekYear() { + return this.isValid + ? weeksInWeekYear( + this.localWeekYear, + this.loc.getMinDaysInFirstWeek(), + this.loc.getStartOfWeek() + ) + : NaN; + } + + /** + * Returns the resolved Intl options for this DateTime. + * This is useful in understanding the behavior of formatting methods + * @param {Object} opts - the same options as toLocaleString + * @return {Object} + */ + resolvedLocaleOptions(opts = {}) { + const { locale, numberingSystem, calendar } = Formatter.create( + this.loc.clone(opts), + opts + ).resolvedOptions(this); + return { locale, numberingSystem, outputCalendar: calendar }; + } + + // TRANSFORM + + /** + * "Set" the DateTime's zone to UTC. Returns a newly-constructed DateTime. + * + * Equivalent to {@link DateTime#setZone}('utc') + * @param {number} [offset=0] - optionally, an offset from UTC in minutes + * @param {Object} [opts={}] - options to pass to `setZone()` + * @return {DateTime} + */ + toUTC(offset = 0, opts = {}) { + return this.setZone(FixedOffsetZone.instance(offset), opts); + } + + /** + * "Set" the DateTime's zone to the host's local zone. Returns a newly-constructed DateTime. + * + * Equivalent to `setZone('local')` + * @return {DateTime} + */ + toLocal() { + return this.setZone(Settings.defaultZone); + } + + /** + * "Set" the DateTime's zone to specified zone. Returns a newly-constructed DateTime. + * + * By default, the setter keeps the underlying time the same (as in, the same timestamp), but the new instance will report different local times and consider DSTs when making computations, as with {@link DateTime#plus}. You may wish to use {@link DateTime#toLocal} and {@link DateTime#toUTC} which provide simple convenience wrappers for commonly used zones. + * @param {string|Zone} [zone='local'] - a zone identifier. As a string, that can be any IANA zone supported by the host environment, or a fixed-offset name of the form 'UTC+3', or the strings 'local' or 'utc'. You may also supply an instance of a {@link DateTime#Zone} class. + * @param {Object} opts - options + * @param {boolean} [opts.keepLocalTime=false] - If true, adjust the underlying time so that the local time stays the same, but in the target zone. You should rarely need this. + * @return {DateTime} + */ + setZone(zone, { keepLocalTime = false, keepCalendarTime = false } = {}) { + zone = normalizeZone(zone, Settings.defaultZone); + if (zone.equals(this.zone)) { + return this; + } else if (!zone.isValid) { + return DateTime.invalid(unsupportedZone(zone)); + } else { + let newTS = this.ts; + if (keepLocalTime || keepCalendarTime) { + const offsetGuess = zone.offset(this.ts); + const asObj = this.toObject(); + [newTS] = objToTS(asObj, offsetGuess, zone); + } + return clone(this, { ts: newTS, zone }); + } + } + + /** + * "Set" the locale, numberingSystem, or outputCalendar. Returns a newly-constructed DateTime. + * @param {Object} properties - the properties to set + * @example DateTime.local(2017, 5, 25).reconfigure({ locale: 'en-GB' }) + * @return {DateTime} + */ + reconfigure({ locale, numberingSystem, outputCalendar } = {}) { + const loc = this.loc.clone({ locale, numberingSystem, outputCalendar }); + return clone(this, { loc }); + } + + /** + * "Set" the locale. Returns a newly-constructed DateTime. + * Just a convenient alias for reconfigure({ locale }) + * @example DateTime.local(2017, 5, 25).setLocale('en-GB') + * @return {DateTime} + */ + setLocale(locale) { + return this.reconfigure({ locale }); + } + + /** + * "Set" the values of specified units. Returns a newly-constructed DateTime. + * You can only set units with this method; for "setting" metadata, see {@link DateTime#reconfigure} and {@link DateTime#setZone}. + * + * This method also supports setting locale-based week units, i.e. `localWeekday`, `localWeekNumber` and `localWeekYear`. + * They cannot be mixed with ISO-week units like `weekday`. + * @param {Object} values - a mapping of units to numbers + * @example dt.set({ year: 2017 }) + * @example dt.set({ hour: 8, minute: 30 }) + * @example dt.set({ weekday: 5 }) + * @example dt.set({ year: 2005, ordinal: 234 }) + * @return {DateTime} + */ + set(values) { + if (!this.isValid) return this; + + const normalized = normalizeObject(values, normalizeUnitWithLocalWeeks); + const { minDaysInFirstWeek, startOfWeek } = usesLocalWeekValues(normalized, this.loc); + + const settingWeekStuff = + !isUndefined(normalized.weekYear) || + !isUndefined(normalized.weekNumber) || + !isUndefined(normalized.weekday), + containsOrdinal = !isUndefined(normalized.ordinal), + containsGregorYear = !isUndefined(normalized.year), + containsGregorMD = !isUndefined(normalized.month) || !isUndefined(normalized.day), + containsGregor = containsGregorYear || containsGregorMD, + definiteWeekDef = normalized.weekYear || normalized.weekNumber; + + if ((containsGregor || containsOrdinal) && definiteWeekDef) { + throw new ConflictingSpecificationError( + "Can't mix weekYear/weekNumber units with year/month/day or ordinals" + ); + } + + if (containsGregorMD && containsOrdinal) { + throw new ConflictingSpecificationError("Can't mix ordinal dates with month/day"); + } + + let mixed; + if (settingWeekStuff) { + mixed = weekToGregorian( + { ...gregorianToWeek(this.c, minDaysInFirstWeek, startOfWeek), ...normalized }, + minDaysInFirstWeek, + startOfWeek + ); + } else if (!isUndefined(normalized.ordinal)) { + mixed = ordinalToGregorian({ ...gregorianToOrdinal(this.c), ...normalized }); + } else { + mixed = { ...this.toObject(), ...normalized }; + + // if we didn't set the day but we ended up on an overflow date, + // use the last day of the right month + if (isUndefined(normalized.day)) { + mixed.day = Math.min(daysInMonth(mixed.year, mixed.month), mixed.day); + } + } + + const [ts, o] = objToTS(mixed, this.o, this.zone); + return clone(this, { ts, o }); + } + + /** + * Add a period of time to this DateTime and return the resulting DateTime + * + * Adding hours, minutes, seconds, or milliseconds increases the timestamp by the right number of milliseconds. Adding days, months, or years shifts the calendar, accounting for DSTs and leap years along the way. Thus, `dt.plus({ hours: 24 })` may result in a different time than `dt.plus({ days: 1 })` if there's a DST shift in between. + * @param {Duration|Object|number} duration - The amount to add. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject() + * @example DateTime.now().plus(123) //~> in 123 milliseconds + * @example DateTime.now().plus({ minutes: 15 }) //~> in 15 minutes + * @example DateTime.now().plus({ days: 1 }) //~> this time tomorrow + * @example DateTime.now().plus({ days: -1 }) //~> this time yesterday + * @example DateTime.now().plus({ hours: 3, minutes: 13 }) //~> in 3 hr, 13 min + * @example DateTime.now().plus(Duration.fromObject({ hours: 3, minutes: 13 })) //~> in 3 hr, 13 min + * @return {DateTime} + */ + plus(duration) { + if (!this.isValid) return this; + const dur = Duration.fromDurationLike(duration); + return clone(this, adjustTime(this, dur)); + } + + /** + * Subtract a period of time to this DateTime and return the resulting DateTime + * See {@link DateTime#plus} + * @param {Duration|Object|number} duration - The amount to subtract. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject() + @return {DateTime} + */ + minus(duration) { + if (!this.isValid) return this; + const dur = Duration.fromDurationLike(duration).negate(); + return clone(this, adjustTime(this, dur)); + } + + /** + * "Set" this DateTime to the beginning of a unit of time. + * @param {string} unit - The unit to go to the beginning of. Can be 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', or 'millisecond'. + * @param {Object} opts - options + * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week + * @example DateTime.local(2014, 3, 3).startOf('month').toISODate(); //=> '2014-03-01' + * @example DateTime.local(2014, 3, 3).startOf('year').toISODate(); //=> '2014-01-01' + * @example DateTime.local(2014, 3, 3).startOf('week').toISODate(); //=> '2014-03-03', weeks always start on Mondays + * @example DateTime.local(2014, 3, 3, 5, 30).startOf('day').toISOTime(); //=> '00:00.000-05:00' + * @example DateTime.local(2014, 3, 3, 5, 30).startOf('hour').toISOTime(); //=> '05:00:00.000-05:00' + * @return {DateTime} + */ + startOf(unit, { useLocaleWeeks = false } = {}) { + if (!this.isValid) return this; + + const o = {}, + normalizedUnit = Duration.normalizeUnit(unit); + switch (normalizedUnit) { + case "years": + o.month = 1; + // falls through + case "quarters": + case "months": + o.day = 1; + // falls through + case "weeks": + case "days": + o.hour = 0; + // falls through + case "hours": + o.minute = 0; + // falls through + case "minutes": + o.second = 0; + // falls through + case "seconds": + o.millisecond = 0; + break; + case "milliseconds": + break; + // no default, invalid units throw in normalizeUnit() + } + + if (normalizedUnit === "weeks") { + if (useLocaleWeeks) { + const startOfWeek = this.loc.getStartOfWeek(); + const { weekday } = this; + if (weekday < startOfWeek) { + o.weekNumber = this.weekNumber - 1; + } + o.weekday = startOfWeek; + } else { + o.weekday = 1; + } + } + + if (normalizedUnit === "quarters") { + const q = Math.ceil(this.month / 3); + o.month = (q - 1) * 3 + 1; + } + + return this.set(o); + } + + /** + * "Set" this DateTime to the end (meaning the last millisecond) of a unit of time + * @param {string} unit - The unit to go to the end of. Can be 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', or 'millisecond'. + * @param {Object} opts - options + * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week + * @example DateTime.local(2014, 3, 3).endOf('month').toISO(); //=> '2014-03-31T23:59:59.999-05:00' + * @example DateTime.local(2014, 3, 3).endOf('year').toISO(); //=> '2014-12-31T23:59:59.999-05:00' + * @example DateTime.local(2014, 3, 3).endOf('week').toISO(); // => '2014-03-09T23:59:59.999-05:00', weeks start on Mondays + * @example DateTime.local(2014, 3, 3, 5, 30).endOf('day').toISO(); //=> '2014-03-03T23:59:59.999-05:00' + * @example DateTime.local(2014, 3, 3, 5, 30).endOf('hour').toISO(); //=> '2014-03-03T05:59:59.999-05:00' + * @return {DateTime} + */ + endOf(unit, opts) { + return this.isValid + ? this.plus({ [unit]: 1 }) + .startOf(unit, opts) + .minus(1) + : this; + } + + // OUTPUT + + /** + * Returns a string representation of this DateTime formatted according to the specified format string. + * **You may not want this.** See {@link DateTime#toLocaleString} for a more flexible formatting tool. For a table of tokens and their interpretations, see [here](https://moment.github.io/luxon/#/formatting?id=table-of-tokens). + * Defaults to en-US if no locale has been specified, regardless of the system's locale. + * @param {string} fmt - the format string + * @param {Object} opts - opts to override the configuration options on this DateTime + * @example DateTime.now().toFormat('yyyy LLL dd') //=> '2017 Apr 22' + * @example DateTime.now().setLocale('fr').toFormat('yyyy LLL dd') //=> '2017 avr. 22' + * @example DateTime.now().toFormat('yyyy LLL dd', { locale: "fr" }) //=> '2017 avr. 22' + * @example DateTime.now().toFormat("HH 'hours and' mm 'minutes'") //=> '20 hours and 55 minutes' + * @return {string} + */ + toFormat(fmt, opts = {}) { + return this.isValid + ? Formatter.create(this.loc.redefaultToEN(opts)).formatDateTimeFromString(this, fmt) + : INVALID; + } + + /** + * Returns a localized string representing this date. Accepts the same options as the Intl.DateTimeFormat constructor and any presets defined by Luxon, such as `DateTime.DATE_FULL` or `DateTime.TIME_SIMPLE`. + * The exact behavior of this method is browser-specific, but in general it will return an appropriate representation + * of the DateTime in the assigned locale. + * Defaults to the system's locale if no locale has been specified + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat + * @param formatOpts {Object} - Intl.DateTimeFormat constructor options and configuration options + * @param {Object} opts - opts to override the configuration options on this DateTime + * @example DateTime.now().toLocaleString(); //=> 4/20/2017 + * @example DateTime.now().setLocale('en-gb').toLocaleString(); //=> '20/04/2017' + * @example DateTime.now().toLocaleString(DateTime.DATE_FULL); //=> 'April 20, 2017' + * @example DateTime.now().toLocaleString(DateTime.DATE_FULL, { locale: 'fr' }); //=> '28 août 2022' + * @example DateTime.now().toLocaleString(DateTime.TIME_SIMPLE); //=> '11:32 AM' + * @example DateTime.now().toLocaleString(DateTime.DATETIME_SHORT); //=> '4/20/2017, 11:32 AM' + * @example DateTime.now().toLocaleString({ weekday: 'long', month: 'long', day: '2-digit' }); //=> 'Thursday, April 20' + * @example DateTime.now().toLocaleString({ weekday: 'short', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); //=> 'Thu, Apr 20, 11:27 AM' + * @example DateTime.now().toLocaleString({ hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }); //=> '11:32' + * @return {string} + */ + toLocaleString(formatOpts = Formats.DATE_SHORT, opts = {}) { + return this.isValid + ? Formatter.create(this.loc.clone(opts), formatOpts).formatDateTime(this) + : INVALID; + } + + /** + * Returns an array of format "parts", meaning individual tokens along with metadata. This is allows callers to post-process individual sections of the formatted output. + * Defaults to the system's locale if no locale has been specified + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/formatToParts + * @param opts {Object} - Intl.DateTimeFormat constructor options, same as `toLocaleString`. + * @example DateTime.now().toLocaleParts(); //=> [ + * //=> { type: 'day', value: '25' }, + * //=> { type: 'literal', value: '/' }, + * //=> { type: 'month', value: '05' }, + * //=> { type: 'literal', value: '/' }, + * //=> { type: 'year', value: '1982' } + * //=> ] + */ + toLocaleParts(opts = {}) { + return this.isValid + ? Formatter.create(this.loc.clone(opts), opts).formatDateTimeParts(this) + : []; + } + + /** + * Returns an ISO 8601-compliant string representation of this DateTime + * @param {Object} opts - options + * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0 + * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0 + * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00' + * @param {boolean} [opts.extendedZone=false] - add the time zone format extension + * @param {string} [opts.format='extended'] - choose between the basic and extended format + * @example DateTime.utc(1983, 5, 25).toISO() //=> '1982-05-25T00:00:00.000Z' + * @example DateTime.now().toISO() //=> '2017-04-22T20:47:05.335-04:00' + * @example DateTime.now().toISO({ includeOffset: false }) //=> '2017-04-22T20:47:05.335' + * @example DateTime.now().toISO({ format: 'basic' }) //=> '20170422T204705.335-0400' + * @return {string} + */ + toISO({ + format = "extended", + suppressSeconds = false, + suppressMilliseconds = false, + includeOffset = true, + extendedZone = false, + } = {}) { + if (!this.isValid) { + return null; + } + + const ext = format === "extended"; + + let c = toISODate(this, ext); + c += "T"; + c += toISOTime(this, ext, suppressSeconds, suppressMilliseconds, includeOffset, extendedZone); + return c; + } + + /** + * Returns an ISO 8601-compliant string representation of this DateTime's date component + * @param {Object} opts - options + * @param {string} [opts.format='extended'] - choose between the basic and extended format + * @example DateTime.utc(1982, 5, 25).toISODate() //=> '1982-05-25' + * @example DateTime.utc(1982, 5, 25).toISODate({ format: 'basic' }) //=> '19820525' + * @return {string} + */ + toISODate({ format = "extended" } = {}) { + if (!this.isValid) { + return null; + } + + return toISODate(this, format === "extended"); + } + + /** + * Returns an ISO 8601-compliant string representation of this DateTime's week date + * @example DateTime.utc(1982, 5, 25).toISOWeekDate() //=> '1982-W21-2' + * @return {string} + */ + toISOWeekDate() { + return toTechFormat(this, "kkkk-'W'WW-c"); + } + + /** + * Returns an ISO 8601-compliant string representation of this DateTime's time component + * @param {Object} opts - options + * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0 + * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0 + * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00' + * @param {boolean} [opts.extendedZone=true] - add the time zone format extension + * @param {boolean} [opts.includePrefix=false] - include the `T` prefix + * @param {string} [opts.format='extended'] - choose between the basic and extended format + * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime() //=> '07:34:19.361Z' + * @example DateTime.utc().set({ hour: 7, minute: 34, seconds: 0, milliseconds: 0 }).toISOTime({ suppressSeconds: true }) //=> '07:34Z' + * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ format: 'basic' }) //=> '073419.361Z' + * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ includePrefix: true }) //=> 'T07:34:19.361Z' + * @return {string} + */ + toISOTime({ + suppressMilliseconds = false, + suppressSeconds = false, + includeOffset = true, + includePrefix = false, + extendedZone = false, + format = "extended", + } = {}) { + if (!this.isValid) { + return null; + } + + let c = includePrefix ? "T" : ""; + return ( + c + + toISOTime( + this, + format === "extended", + suppressSeconds, + suppressMilliseconds, + includeOffset, + extendedZone + ) + ); + } + + /** + * Returns an RFC 2822-compatible string representation of this DateTime + * @example DateTime.utc(2014, 7, 13).toRFC2822() //=> 'Sun, 13 Jul 2014 00:00:00 +0000' + * @example DateTime.local(2014, 7, 13).toRFC2822() //=> 'Sun, 13 Jul 2014 00:00:00 -0400' + * @return {string} + */ + toRFC2822() { + return toTechFormat(this, "EEE, dd LLL yyyy HH:mm:ss ZZZ", false); + } + + /** + * Returns a string representation of this DateTime appropriate for use in HTTP headers. The output is always expressed in GMT. + * Specifically, the string conforms to RFC 1123. + * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1 + * @example DateTime.utc(2014, 7, 13).toHTTP() //=> 'Sun, 13 Jul 2014 00:00:00 GMT' + * @example DateTime.utc(2014, 7, 13, 19).toHTTP() //=> 'Sun, 13 Jul 2014 19:00:00 GMT' + * @return {string} + */ + toHTTP() { + return toTechFormat(this.toUTC(), "EEE, dd LLL yyyy HH:mm:ss 'GMT'"); + } + + /** + * Returns a string representation of this DateTime appropriate for use in SQL Date + * @example DateTime.utc(2014, 7, 13).toSQLDate() //=> '2014-07-13' + * @return {string} + */ + toSQLDate() { + if (!this.isValid) { + return null; + } + return toISODate(this, true); + } + + /** + * Returns a string representation of this DateTime appropriate for use in SQL Time + * @param {Object} opts - options + * @param {boolean} [opts.includeZone=false] - include the zone, such as 'America/New_York'. Overrides includeOffset. + * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00' + * @param {boolean} [opts.includeOffsetSpace=true] - include the space between the time and the offset, such as '05:15:16.345 -04:00' + * @example DateTime.utc().toSQL() //=> '05:15:16.345' + * @example DateTime.now().toSQL() //=> '05:15:16.345 -04:00' + * @example DateTime.now().toSQL({ includeOffset: false }) //=> '05:15:16.345' + * @example DateTime.now().toSQL({ includeZone: false }) //=> '05:15:16.345 America/New_York' + * @return {string} + */ + toSQLTime({ includeOffset = true, includeZone = false, includeOffsetSpace = true } = {}) { + let fmt = "HH:mm:ss.SSS"; + + if (includeZone || includeOffset) { + if (includeOffsetSpace) { + fmt += " "; + } + if (includeZone) { + fmt += "z"; + } else if (includeOffset) { + fmt += "ZZ"; + } + } + + return toTechFormat(this, fmt, true); + } + + /** + * Returns a string representation of this DateTime appropriate for use in SQL DateTime + * @param {Object} opts - options + * @param {boolean} [opts.includeZone=false] - include the zone, such as 'America/New_York'. Overrides includeOffset. + * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00' + * @param {boolean} [opts.includeOffsetSpace=true] - include the space between the time and the offset, such as '05:15:16.345 -04:00' + * @example DateTime.utc(2014, 7, 13).toSQL() //=> '2014-07-13 00:00:00.000 Z' + * @example DateTime.local(2014, 7, 13).toSQL() //=> '2014-07-13 00:00:00.000 -04:00' + * @example DateTime.local(2014, 7, 13).toSQL({ includeOffset: false }) //=> '2014-07-13 00:00:00.000' + * @example DateTime.local(2014, 7, 13).toSQL({ includeZone: true }) //=> '2014-07-13 00:00:00.000 America/New_York' + * @return {string} + */ + toSQL(opts = {}) { + if (!this.isValid) { + return null; + } + + return `${this.toSQLDate()} ${this.toSQLTime(opts)}`; + } + + /** + * Returns a string representation of this DateTime appropriate for debugging + * @return {string} + */ + toString() { + return this.isValid ? this.toISO() : INVALID; + } + + /** + * Returns a string representation of this DateTime appropriate for the REPL. + * @return {string} + */ + [Symbol.for("nodejs.util.inspect.custom")]() { + if (this.isValid) { + return `DateTime { ts: ${this.toISO()}, zone: ${this.zone.name}, locale: ${this.locale} }`; + } else { + return `DateTime { Invalid, reason: ${this.invalidReason} }`; + } + } + + /** + * Returns the epoch milliseconds of this DateTime. Alias of {@link DateTime#toMillis} + * @return {number} + */ + valueOf() { + return this.toMillis(); + } + + /** + * Returns the epoch milliseconds of this DateTime. + * @return {number} + */ + toMillis() { + return this.isValid ? this.ts : NaN; + } + + /** + * Returns the epoch seconds of this DateTime. + * @return {number} + */ + toSeconds() { + return this.isValid ? this.ts / 1000 : NaN; + } + + /** + * Returns the epoch seconds (as a whole number) of this DateTime. + * @return {number} + */ + toUnixInteger() { + return this.isValid ? Math.floor(this.ts / 1000) : NaN; + } + + /** + * Returns an ISO 8601 representation of this DateTime appropriate for use in JSON. + * @return {string} + */ + toJSON() { + return this.toISO(); + } + + /** + * Returns a BSON serializable equivalent to this DateTime. + * @return {Date} + */ + toBSON() { + return this.toJSDate(); + } + + /** + * Returns a JavaScript object with this DateTime's year, month, day, and so on. + * @param opts - options for generating the object + * @param {boolean} [opts.includeConfig=false] - include configuration attributes in the output + * @example DateTime.now().toObject() //=> { year: 2017, month: 4, day: 22, hour: 20, minute: 49, second: 42, millisecond: 268 } + * @return {Object} + */ + toObject(opts = {}) { + if (!this.isValid) return {}; + + const base = { ...this.c }; + + if (opts.includeConfig) { + base.outputCalendar = this.outputCalendar; + base.numberingSystem = this.loc.numberingSystem; + base.locale = this.loc.locale; + } + return base; + } + + /** + * Returns a JavaScript Date equivalent to this DateTime. + * @return {Date} + */ + toJSDate() { + return new Date(this.isValid ? this.ts : NaN); + } + + // COMPARE + + /** + * Return the difference between two DateTimes as a Duration. + * @param {DateTime} otherDateTime - the DateTime to compare this one to + * @param {string|string[]} [unit=['milliseconds']] - the unit or array of units (such as 'hours' or 'days') to include in the duration. + * @param {Object} opts - options that affect the creation of the Duration + * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use + * @example + * var i1 = DateTime.fromISO('1982-05-25T09:45'), + * i2 = DateTime.fromISO('1983-10-14T10:30'); + * i2.diff(i1).toObject() //=> { milliseconds: 43807500000 } + * i2.diff(i1, 'hours').toObject() //=> { hours: 12168.75 } + * i2.diff(i1, ['months', 'days']).toObject() //=> { months: 16, days: 19.03125 } + * i2.diff(i1, ['months', 'days', 'hours']).toObject() //=> { months: 16, days: 19, hours: 0.75 } + * @return {Duration} + */ + diff(otherDateTime, unit = "milliseconds", opts = {}) { + if (!this.isValid || !otherDateTime.isValid) { + return Duration.invalid("created by diffing an invalid DateTime"); + } + + const durOpts = { locale: this.locale, numberingSystem: this.numberingSystem, ...opts }; + + const units = maybeArray(unit).map(Duration.normalizeUnit), + otherIsLater = otherDateTime.valueOf() > this.valueOf(), + earlier = otherIsLater ? this : otherDateTime, + later = otherIsLater ? otherDateTime : this, + diffed = diff(earlier, later, units, durOpts); + + return otherIsLater ? diffed.negate() : diffed; + } + + /** + * Return the difference between this DateTime and right now. + * See {@link DateTime#diff} + * @param {string|string[]} [unit=['milliseconds']] - the unit or units units (such as 'hours' or 'days') to include in the duration + * @param {Object} opts - options that affect the creation of the Duration + * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use + * @return {Duration} + */ + diffNow(unit = "milliseconds", opts = {}) { + return this.diff(DateTime.now(), unit, opts); + } + + /** + * Return an Interval spanning between this DateTime and another DateTime + * @param {DateTime} otherDateTime - the other end point of the Interval + * @return {Interval} + */ + until(otherDateTime) { + return this.isValid ? Interval.fromDateTimes(this, otherDateTime) : this; + } + + /** + * Return whether this DateTime is in the same unit of time as another DateTime. + * Higher-order units must also be identical for this function to return `true`. + * Note that time zones are **ignored** in this comparison, which compares the **local** calendar time. Use {@link DateTime#setZone} to convert one of the dates if needed. + * @param {DateTime} otherDateTime - the other DateTime + * @param {string} unit - the unit of time to check sameness on + * @param {Object} opts - options + * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week; only the locale of this DateTime is used + * @example DateTime.now().hasSame(otherDT, 'day'); //~> true if otherDT is in the same current calendar day + * @return {boolean} + */ + hasSame(otherDateTime, unit, opts) { + if (!this.isValid) return false; + + const inputMs = otherDateTime.valueOf(); + const adjustedToZone = this.setZone(otherDateTime.zone, { keepLocalTime: true }); + return ( + adjustedToZone.startOf(unit, opts) <= inputMs && inputMs <= adjustedToZone.endOf(unit, opts) + ); + } + + /** + * Equality check + * Two DateTimes are equal if and only if they represent the same millisecond, have the same zone and location, and are both valid. + * To compare just the millisecond values, use `+dt1 === +dt2`. + * @param {DateTime} other - the other DateTime + * @return {boolean} + */ + equals(other) { + return ( + this.isValid && + other.isValid && + this.valueOf() === other.valueOf() && + this.zone.equals(other.zone) && + this.loc.equals(other.loc) + ); + } + + /** + * Returns a string representation of a this time relative to now, such as "in two days". Can only internationalize if your + * platform supports Intl.RelativeTimeFormat. Rounds down by default. + * @param {Object} options - options that affect the output + * @param {DateTime} [options.base=DateTime.now()] - the DateTime to use as the basis to which this time is compared. Defaults to now. + * @param {string} [options.style="long"] - the style of units, must be "long", "short", or "narrow" + * @param {string|string[]} options.unit - use a specific unit or array of units; if omitted, or an array, the method will pick the best unit. Use an array or one of "years", "quarters", "months", "weeks", "days", "hours", "minutes", or "seconds" + * @param {boolean} [options.round=true] - whether to round the numbers in the output. + * @param {number} [options.padding=0] - padding in milliseconds. This allows you to round up the result if it fits inside the threshold. Don't use in combination with {round: false} because the decimal output will include the padding. + * @param {string} options.locale - override the locale of this DateTime + * @param {string} options.numberingSystem - override the numberingSystem of this DateTime. The Intl system may choose not to honor this + * @example DateTime.now().plus({ days: 1 }).toRelative() //=> "in 1 day" + * @example DateTime.now().setLocale("es").toRelative({ days: 1 }) //=> "dentro de 1 día" + * @example DateTime.now().plus({ days: 1 }).toRelative({ locale: "fr" }) //=> "dans 23 heures" + * @example DateTime.now().minus({ days: 2 }).toRelative() //=> "2 days ago" + * @example DateTime.now().minus({ days: 2 }).toRelative({ unit: "hours" }) //=> "48 hours ago" + * @example DateTime.now().minus({ hours: 36 }).toRelative({ round: false }) //=> "1.5 days ago" + */ + toRelative(options = {}) { + if (!this.isValid) return null; + const base = options.base || DateTime.fromObject({}, { zone: this.zone }), + padding = options.padding ? (this < base ? -options.padding : options.padding) : 0; + let units = ["years", "months", "days", "hours", "minutes", "seconds"]; + let unit = options.unit; + if (Array.isArray(options.unit)) { + units = options.unit; + unit = undefined; + } + return diffRelative(base, this.plus(padding), { + ...options, + numeric: "always", + units, + unit, + }); + } + + /** + * Returns a string representation of this date relative to today, such as "yesterday" or "next month". + * Only internationalizes on platforms that supports Intl.RelativeTimeFormat. + * @param {Object} options - options that affect the output + * @param {DateTime} [options.base=DateTime.now()] - the DateTime to use as the basis to which this time is compared. Defaults to now. + * @param {string} options.locale - override the locale of this DateTime + * @param {string} options.unit - use a specific unit; if omitted, the method will pick the unit. Use one of "years", "quarters", "months", "weeks", or "days" + * @param {string} options.numberingSystem - override the numberingSystem of this DateTime. The Intl system may choose not to honor this + * @example DateTime.now().plus({ days: 1 }).toRelativeCalendar() //=> "tomorrow" + * @example DateTime.now().setLocale("es").plus({ days: 1 }).toRelative() //=> ""mañana" + * @example DateTime.now().plus({ days: 1 }).toRelativeCalendar({ locale: "fr" }) //=> "demain" + * @example DateTime.now().minus({ days: 2 }).toRelativeCalendar() //=> "2 days ago" + */ + toRelativeCalendar(options = {}) { + if (!this.isValid) return null; + + return diffRelative(options.base || DateTime.fromObject({}, { zone: this.zone }), this, { + ...options, + numeric: "auto", + units: ["years", "months", "days"], + calendary: true, + }); + } + + /** + * Return the min of several date times + * @param {...DateTime} dateTimes - the DateTimes from which to choose the minimum + * @return {DateTime} the min DateTime, or undefined if called with no argument + */ + static min(...dateTimes) { + if (!dateTimes.every(DateTime.isDateTime)) { + throw new InvalidArgumentError("min requires all arguments be DateTimes"); + } + return bestBy(dateTimes, (i) => i.valueOf(), Math.min); + } + + /** + * Return the max of several date times + * @param {...DateTime} dateTimes - the DateTimes from which to choose the maximum + * @return {DateTime} the max DateTime, or undefined if called with no argument + */ + static max(...dateTimes) { + if (!dateTimes.every(DateTime.isDateTime)) { + throw new InvalidArgumentError("max requires all arguments be DateTimes"); + } + return bestBy(dateTimes, (i) => i.valueOf(), Math.max); + } + + // MISC + + /** + * Explain how a string would be parsed by fromFormat() + * @param {string} text - the string to parse + * @param {string} fmt - the format the string is expected to be in (see description) + * @param {Object} options - options taken by fromFormat() + * @return {Object} + */ + static fromFormatExplain(text, fmt, options = {}) { + const { locale = null, numberingSystem = null } = options, + localeToUse = Locale.fromOpts({ + locale, + numberingSystem, + defaultToEN: true, + }); + return explainFromTokens(localeToUse, text, fmt); + } + + /** + * @deprecated use fromFormatExplain instead + */ + static fromStringExplain(text, fmt, options = {}) { + return DateTime.fromFormatExplain(text, fmt, options); + } + + // FORMAT PRESETS + + /** + * {@link DateTime#toLocaleString} format like 10/14/1983 + * @type {Object} + */ + static get DATE_SHORT() { + return Formats.DATE_SHORT; + } + + /** + * {@link DateTime#toLocaleString} format like 'Oct 14, 1983' + * @type {Object} + */ + static get DATE_MED() { + return Formats.DATE_MED; + } + + /** + * {@link DateTime#toLocaleString} format like 'Fri, Oct 14, 1983' + * @type {Object} + */ + static get DATE_MED_WITH_WEEKDAY() { + return Formats.DATE_MED_WITH_WEEKDAY; + } + + /** + * {@link DateTime#toLocaleString} format like 'October 14, 1983' + * @type {Object} + */ + static get DATE_FULL() { + return Formats.DATE_FULL; + } + + /** + * {@link DateTime#toLocaleString} format like 'Tuesday, October 14, 1983' + * @type {Object} + */ + static get DATE_HUGE() { + return Formats.DATE_HUGE; + } + + /** + * {@link DateTime#toLocaleString} format like '09:30 AM'. Only 12-hour if the locale is. + * @type {Object} + */ + static get TIME_SIMPLE() { + return Formats.TIME_SIMPLE; + } + + /** + * {@link DateTime#toLocaleString} format like '09:30:23 AM'. Only 12-hour if the locale is. + * @type {Object} + */ + static get TIME_WITH_SECONDS() { + return Formats.TIME_WITH_SECONDS; + } + + /** + * {@link DateTime#toLocaleString} format like '09:30:23 AM EDT'. Only 12-hour if the locale is. + * @type {Object} + */ + static get TIME_WITH_SHORT_OFFSET() { + return Formats.TIME_WITH_SHORT_OFFSET; + } + + /** + * {@link DateTime#toLocaleString} format like '09:30:23 AM Eastern Daylight Time'. Only 12-hour if the locale is. + * @type {Object} + */ + static get TIME_WITH_LONG_OFFSET() { + return Formats.TIME_WITH_LONG_OFFSET; + } + + /** + * {@link DateTime#toLocaleString} format like '09:30', always 24-hour. + * @type {Object} + */ + static get TIME_24_SIMPLE() { + return Formats.TIME_24_SIMPLE; + } + + /** + * {@link DateTime#toLocaleString} format like '09:30:23', always 24-hour. + * @type {Object} + */ + static get TIME_24_WITH_SECONDS() { + return Formats.TIME_24_WITH_SECONDS; + } + + /** + * {@link DateTime#toLocaleString} format like '09:30:23 EDT', always 24-hour. + * @type {Object} + */ + static get TIME_24_WITH_SHORT_OFFSET() { + return Formats.TIME_24_WITH_SHORT_OFFSET; + } + + /** + * {@link DateTime#toLocaleString} format like '09:30:23 Eastern Daylight Time', always 24-hour. + * @type {Object} + */ + static get TIME_24_WITH_LONG_OFFSET() { + return Formats.TIME_24_WITH_LONG_OFFSET; + } + + /** + * {@link DateTime#toLocaleString} format like '10/14/1983, 9:30 AM'. Only 12-hour if the locale is. + * @type {Object} + */ + static get DATETIME_SHORT() { + return Formats.DATETIME_SHORT; + } + + /** + * {@link DateTime#toLocaleString} format like '10/14/1983, 9:30:33 AM'. Only 12-hour if the locale is. + * @type {Object} + */ + static get DATETIME_SHORT_WITH_SECONDS() { + return Formats.DATETIME_SHORT_WITH_SECONDS; + } + + /** + * {@link DateTime#toLocaleString} format like 'Oct 14, 1983, 9:30 AM'. Only 12-hour if the locale is. + * @type {Object} + */ + static get DATETIME_MED() { + return Formats.DATETIME_MED; + } + + /** + * {@link DateTime#toLocaleString} format like 'Oct 14, 1983, 9:30:33 AM'. Only 12-hour if the locale is. + * @type {Object} + */ + static get DATETIME_MED_WITH_SECONDS() { + return Formats.DATETIME_MED_WITH_SECONDS; + } + + /** + * {@link DateTime#toLocaleString} format like 'Fri, 14 Oct 1983, 9:30 AM'. Only 12-hour if the locale is. + * @type {Object} + */ + static get DATETIME_MED_WITH_WEEKDAY() { + return Formats.DATETIME_MED_WITH_WEEKDAY; + } + + /** + * {@link DateTime#toLocaleString} format like 'October 14, 1983, 9:30 AM EDT'. Only 12-hour if the locale is. + * @type {Object} + */ + static get DATETIME_FULL() { + return Formats.DATETIME_FULL; + } + + /** + * {@link DateTime#toLocaleString} format like 'October 14, 1983, 9:30:33 AM EDT'. Only 12-hour if the locale is. + * @type {Object} + */ + static get DATETIME_FULL_WITH_SECONDS() { + return Formats.DATETIME_FULL_WITH_SECONDS; + } + + /** + * {@link DateTime#toLocaleString} format like 'Friday, October 14, 1983, 9:30 AM Eastern Daylight Time'. Only 12-hour if the locale is. + * @type {Object} + */ + static get DATETIME_HUGE() { + return Formats.DATETIME_HUGE; + } + + /** + * {@link DateTime#toLocaleString} format like 'Friday, October 14, 1983, 9:30:33 AM Eastern Daylight Time'. Only 12-hour if the locale is. + * @type {Object} + */ + static get DATETIME_HUGE_WITH_SECONDS() { + return Formats.DATETIME_HUGE_WITH_SECONDS; + } +} + +/** + * @private + */ +export function friendlyDateTime(dateTimeish) { + if (DateTime.isDateTime(dateTimeish)) { + return dateTimeish; + } else if (dateTimeish && dateTimeish.valueOf && isNumber(dateTimeish.valueOf())) { + return DateTime.fromJSDate(dateTimeish); + } else if (dateTimeish && typeof dateTimeish === "object") { + return DateTime.fromObject(dateTimeish); + } else { + throw new InvalidArgumentError( + `Unknown datetime argument: ${dateTimeish}, of type ${typeof dateTimeish}` + ); + } +} diff --git a/src/shared/libs/luxon/src/duration.js b/src/shared/libs/luxon/src/duration.js new file mode 100644 index 0000000..c1135de --- /dev/null +++ b/src/shared/libs/luxon/src/duration.js @@ -0,0 +1,990 @@ +import { InvalidArgumentError, InvalidDurationError, InvalidUnitError } from "./errors.js"; +import Formatter from "./impl/formatter.js"; +import Invalid from "./impl/invalid.js"; +import Locale from "./impl/locale.js"; +import { parseISODuration, parseISOTimeOnly } from "./impl/regexParser.js"; +import { + asNumber, + hasOwnProperty, + isNumber, + isUndefined, + normalizeObject, + roundTo, +} from "./impl/util.js"; +import Settings from "./settings.js"; +import DateTime from "./datetime.js"; + +const INVALID = "Invalid Duration"; + +// unit conversion constants +export const lowOrderMatrix = { + weeks: { + days: 7, + hours: 7 * 24, + minutes: 7 * 24 * 60, + seconds: 7 * 24 * 60 * 60, + milliseconds: 7 * 24 * 60 * 60 * 1000, + }, + days: { + hours: 24, + minutes: 24 * 60, + seconds: 24 * 60 * 60, + milliseconds: 24 * 60 * 60 * 1000, + }, + hours: { minutes: 60, seconds: 60 * 60, milliseconds: 60 * 60 * 1000 }, + minutes: { seconds: 60, milliseconds: 60 * 1000 }, + seconds: { milliseconds: 1000 }, + }, + casualMatrix = { + years: { + quarters: 4, + months: 12, + weeks: 52, + days: 365, + hours: 365 * 24, + minutes: 365 * 24 * 60, + seconds: 365 * 24 * 60 * 60, + milliseconds: 365 * 24 * 60 * 60 * 1000, + }, + quarters: { + months: 3, + weeks: 13, + days: 91, + hours: 91 * 24, + minutes: 91 * 24 * 60, + seconds: 91 * 24 * 60 * 60, + milliseconds: 91 * 24 * 60 * 60 * 1000, + }, + months: { + weeks: 4, + days: 30, + hours: 30 * 24, + minutes: 30 * 24 * 60, + seconds: 30 * 24 * 60 * 60, + milliseconds: 30 * 24 * 60 * 60 * 1000, + }, + + ...lowOrderMatrix, + }, + daysInYearAccurate = 146097.0 / 400, + daysInMonthAccurate = 146097.0 / 4800, + accurateMatrix = { + years: { + quarters: 4, + months: 12, + weeks: daysInYearAccurate / 7, + days: daysInYearAccurate, + hours: daysInYearAccurate * 24, + minutes: daysInYearAccurate * 24 * 60, + seconds: daysInYearAccurate * 24 * 60 * 60, + milliseconds: daysInYearAccurate * 24 * 60 * 60 * 1000, + }, + quarters: { + months: 3, + weeks: daysInYearAccurate / 28, + days: daysInYearAccurate / 4, + hours: (daysInYearAccurate * 24) / 4, + minutes: (daysInYearAccurate * 24 * 60) / 4, + seconds: (daysInYearAccurate * 24 * 60 * 60) / 4, + milliseconds: (daysInYearAccurate * 24 * 60 * 60 * 1000) / 4, + }, + months: { + weeks: daysInMonthAccurate / 7, + days: daysInMonthAccurate, + hours: daysInMonthAccurate * 24, + minutes: daysInMonthAccurate * 24 * 60, + seconds: daysInMonthAccurate * 24 * 60 * 60, + milliseconds: daysInMonthAccurate * 24 * 60 * 60 * 1000, + }, + ...lowOrderMatrix, + }; + +// units ordered by size +const orderedUnits = [ + "years", + "quarters", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", + "milliseconds", +]; + +const reverseUnits = orderedUnits.slice(0).reverse(); + +// clone really means "create another instance just like this one, but with these changes" +function clone(dur, alts, clear = false) { + // deep merge for vals + const conf = { + values: clear ? alts.values : { ...dur.values, ...(alts.values || {}) }, + loc: dur.loc.clone(alts.loc), + conversionAccuracy: alts.conversionAccuracy || dur.conversionAccuracy, + matrix: alts.matrix || dur.matrix, + }; + return new Duration(conf); +} + +function durationToMillis(matrix, vals) { + let sum = vals.milliseconds ?? 0; + for (const unit of reverseUnits.slice(1)) { + if (vals[unit]) { + sum += vals[unit] * matrix[unit]["milliseconds"]; + } + } + return sum; +} + +// NB: mutates parameters +function normalizeValues(matrix, vals) { + // the logic below assumes the overall value of the duration is positive + // if this is not the case, factor is used to make it so + const factor = durationToMillis(matrix, vals) < 0 ? -1 : 1; + + orderedUnits.reduceRight((previous, current) => { + if (!isUndefined(vals[current])) { + if (previous) { + const previousVal = vals[previous] * factor; + const conv = matrix[current][previous]; + + // if (previousVal < 0): + // lower order unit is negative (e.g. { years: 2, days: -2 }) + // normalize this by reducing the higher order unit by the appropriate amount + // and increasing the lower order unit + // this can never make the higher order unit negative, because this function only operates + // on positive durations, so the amount of time represented by the lower order unit cannot + // be larger than the higher order unit + // else: + // lower order unit is positive (e.g. { years: 2, days: 450 } or { years: -2, days: 450 }) + // in this case we attempt to convert as much as possible from the lower order unit into + // the higher order one + // + // Math.floor takes care of both of these cases, rounding away from 0 + // if previousVal < 0 it makes the absolute value larger + // if previousVal >= it makes the absolute value smaller + const rollUp = Math.floor(previousVal / conv); + vals[current] += rollUp * factor; + vals[previous] -= rollUp * conv * factor; + } + return current; + } else { + return previous; + } + }, null); + + // try to convert any decimals into smaller units if possible + // for example for { years: 2.5, days: 0, seconds: 0 } we want to get { years: 2, days: 182, hours: 12 } + orderedUnits.reduce((previous, current) => { + if (!isUndefined(vals[current])) { + if (previous) { + const fraction = vals[previous] % 1; + vals[previous] -= fraction; + vals[current] += fraction * matrix[previous][current]; + } + return current; + } else { + return previous; + } + }, null); +} + +// Remove all properties with a value of 0 from an object +function removeZeroes(vals) { + const newVals = {}; + for (const [key, value] of Object.entries(vals)) { + if (value !== 0) { + newVals[key] = value; + } + } + return newVals; +} + +/** + * A Duration object represents a period of time, like "2 months" or "1 day, 1 hour". Conceptually, it's just a map of units to their quantities, accompanied by some additional configuration and methods for creating, parsing, interrogating, transforming, and formatting them. They can be used on their own or in conjunction with other Luxon types; for example, you can use {@link DateTime#plus} to add a Duration object to a DateTime, producing another DateTime. + * + * Here is a brief overview of commonly used methods and getters in Duration: + * + * * **Creation** To create a Duration, use {@link Duration.fromMillis}, {@link Duration.fromObject}, or {@link Duration.fromISO}. + * * **Unit values** See the {@link Duration#years}, {@link Duration#months}, {@link Duration#weeks}, {@link Duration#days}, {@link Duration#hours}, {@link Duration#minutes}, {@link Duration#seconds}, {@link Duration#milliseconds} accessors. + * * **Configuration** See {@link Duration#locale} and {@link Duration#numberingSystem} accessors. + * * **Transformation** To create new Durations out of old ones use {@link Duration#plus}, {@link Duration#minus}, {@link Duration#normalize}, {@link Duration#set}, {@link Duration#reconfigure}, {@link Duration#shiftTo}, and {@link Duration#negate}. + * * **Output** To convert the Duration into other representations, see {@link Duration#as}, {@link Duration#toISO}, {@link Duration#toFormat}, and {@link Duration#toJSON} + * + * There's are more methods documented below. In addition, for more information on subtler topics like internationalization and validity, see the external documentation. + */ +export default class Duration { + /** + * @private + */ + constructor(config) { + const accurate = config.conversionAccuracy === "longterm" || false; + let matrix = accurate ? accurateMatrix : casualMatrix; + + if (config.matrix) { + matrix = config.matrix; + } + + /** + * @access private + */ + this.values = config.values; + /** + * @access private + */ + this.loc = config.loc || Locale.create(); + /** + * @access private + */ + this.conversionAccuracy = accurate ? "longterm" : "casual"; + /** + * @access private + */ + this.invalid = config.invalid || null; + /** + * @access private + */ + this.matrix = matrix; + /** + * @access private + */ + this.isLuxonDuration = true; + } + + /** + * Create Duration from a number of milliseconds. + * @param {number} count of milliseconds + * @param {Object} opts - options for parsing + * @param {string} [opts.locale='en-US'] - the locale to use + * @param {string} opts.numberingSystem - the numbering system to use + * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use + * @return {Duration} + */ + static fromMillis(count, opts) { + return Duration.fromObject({ milliseconds: count }, opts); + } + + /** + * Create a Duration from a JavaScript object with keys like 'years' and 'hours'. + * If this object is empty then a zero milliseconds duration is returned. + * @param {Object} obj - the object to create the DateTime from + * @param {number} obj.years + * @param {number} obj.quarters + * @param {number} obj.months + * @param {number} obj.weeks + * @param {number} obj.days + * @param {number} obj.hours + * @param {number} obj.minutes + * @param {number} obj.seconds + * @param {number} obj.milliseconds + * @param {Object} [opts=[]] - options for creating this Duration + * @param {string} [opts.locale='en-US'] - the locale to use + * @param {string} opts.numberingSystem - the numbering system to use + * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use + * @param {string} [opts.matrix=Object] - the custom conversion system to use + * @return {Duration} + */ + static fromObject(obj, opts = {}) { + if (obj == null || typeof obj !== "object") { + throw new InvalidArgumentError( + `Duration.fromObject: argument expected to be an object, got ${ + obj === null ? "null" : typeof obj + }` + ); + } + + return new Duration({ + values: normalizeObject(obj, Duration.normalizeUnit), + loc: Locale.fromObject(opts), + conversionAccuracy: opts.conversionAccuracy, + matrix: opts.matrix, + }); + } + + /** + * Create a Duration from DurationLike. + * + * @param {Object | number | Duration} durationLike + * One of: + * - object with keys like 'years' and 'hours'. + * - number representing milliseconds + * - Duration instance + * @return {Duration} + */ + static fromDurationLike(durationLike) { + if (isNumber(durationLike)) { + return Duration.fromMillis(durationLike); + } else if (Duration.isDuration(durationLike)) { + return durationLike; + } else if (typeof durationLike === "object") { + return Duration.fromObject(durationLike); + } else { + throw new InvalidArgumentError( + `Unknown duration argument ${durationLike} of type ${typeof durationLike}` + ); + } + } + + /** + * Create a Duration from an ISO 8601 duration string. + * @param {string} text - text to parse + * @param {Object} opts - options for parsing + * @param {string} [opts.locale='en-US'] - the locale to use + * @param {string} opts.numberingSystem - the numbering system to use + * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use + * @param {string} [opts.matrix=Object] - the preset conversion system to use + * @see https://en.wikipedia.org/wiki/ISO_8601#Durations + * @example Duration.fromISO('P3Y6M1W4DT12H30M5S').toObject() //=> { years: 3, months: 6, weeks: 1, days: 4, hours: 12, minutes: 30, seconds: 5 } + * @example Duration.fromISO('PT23H').toObject() //=> { hours: 23 } + * @example Duration.fromISO('P5Y3M').toObject() //=> { years: 5, months: 3 } + * @return {Duration} + */ + static fromISO(text, opts) { + const [parsed] = parseISODuration(text); + if (parsed) { + return Duration.fromObject(parsed, opts); + } else { + return Duration.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`); + } + } + + /** + * Create a Duration from an ISO 8601 time string. + * @param {string} text - text to parse + * @param {Object} opts - options for parsing + * @param {string} [opts.locale='en-US'] - the locale to use + * @param {string} opts.numberingSystem - the numbering system to use + * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use + * @param {string} [opts.matrix=Object] - the conversion system to use + * @see https://en.wikipedia.org/wiki/ISO_8601#Times + * @example Duration.fromISOTime('11:22:33.444').toObject() //=> { hours: 11, minutes: 22, seconds: 33, milliseconds: 444 } + * @example Duration.fromISOTime('11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 } + * @example Duration.fromISOTime('T11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 } + * @example Duration.fromISOTime('1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 } + * @example Duration.fromISOTime('T1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 } + * @return {Duration} + */ + static fromISOTime(text, opts) { + const [parsed] = parseISOTimeOnly(text); + if (parsed) { + return Duration.fromObject(parsed, opts); + } else { + return Duration.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`); + } + } + + /** + * Create an invalid Duration. + * @param {string} reason - simple string of why this datetime is invalid. Should not contain parameters or anything else data-dependent + * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information + * @return {Duration} + */ + static invalid(reason, explanation = null) { + if (!reason) { + throw new InvalidArgumentError("need to specify a reason the Duration is invalid"); + } + + const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation); + + if (Settings.throwOnInvalid) { + throw new InvalidDurationError(invalid); + } else { + return new Duration({ invalid }); + } + } + + /** + * @private + */ + static normalizeUnit(unit) { + const normalized = { + year: "years", + years: "years", + quarter: "quarters", + quarters: "quarters", + month: "months", + months: "months", + week: "weeks", + weeks: "weeks", + day: "days", + days: "days", + hour: "hours", + hours: "hours", + minute: "minutes", + minutes: "minutes", + second: "seconds", + seconds: "seconds", + millisecond: "milliseconds", + milliseconds: "milliseconds", + }[unit ? unit.toLowerCase() : unit]; + + if (!normalized) throw new InvalidUnitError(unit); + + return normalized; + } + + /** + * Check if an object is a Duration. Works across context boundaries + * @param {object} o + * @return {boolean} + */ + static isDuration(o) { + return (o && o.isLuxonDuration) || false; + } + + /** + * Get the locale of a Duration, such 'en-GB' + * @type {string} + */ + get locale() { + return this.isValid ? this.loc.locale : null; + } + + /** + * Get the numbering system of a Duration, such 'beng'. The numbering system is used when formatting the Duration + * + * @type {string} + */ + get numberingSystem() { + return this.isValid ? this.loc.numberingSystem : null; + } + + /** + * Returns a string representation of this Duration formatted according to the specified format string. You may use these tokens: + * * `S` for milliseconds + * * `s` for seconds + * * `m` for minutes + * * `h` for hours + * * `d` for days + * * `w` for weeks + * * `M` for months + * * `y` for years + * Notes: + * * Add padding by repeating the token, e.g. "yy" pads the years to two digits, "hhhh" pads the hours out to four digits + * * Tokens can be escaped by wrapping with single quotes. + * * The duration will be converted to the set of units in the format string using {@link Duration#shiftTo} and the Durations's conversion accuracy setting. + * @param {string} fmt - the format string + * @param {Object} opts - options + * @param {boolean} [opts.floor=true] - floor numerical values + * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("y d s") //=> "1 6 2" + * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("yy dd sss") //=> "01 06 002" + * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("M S") //=> "12 518402000" + * @return {string} + */ + toFormat(fmt, opts = {}) { + // reverse-compat since 1.2; we always round down now, never up, and we do it by default + const fmtOpts = { + ...opts, + floor: opts.round !== false && opts.floor !== false, + }; + return this.isValid + ? Formatter.create(this.loc, fmtOpts).formatDurationFromString(this, fmt) + : INVALID; + } + + /** + * Returns a string representation of a Duration with all units included. + * To modify its behavior, use `listStyle` and any Intl.NumberFormat option, though `unitDisplay` is especially relevant. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options + * @param {Object} opts - Formatting options. Accepts the same keys as the options parameter of the native `Intl.NumberFormat` constructor, as well as `listStyle`. + * @param {string} [opts.listStyle='narrow'] - How to format the merged list. Corresponds to the `style` property of the options parameter of the native `Intl.ListFormat` constructor. + * @example + * ```js + * var dur = Duration.fromObject({ days: 1, hours: 5, minutes: 6 }) + * dur.toHuman() //=> '1 day, 5 hours, 6 minutes' + * dur.toHuman({ listStyle: "long" }) //=> '1 day, 5 hours, and 6 minutes' + * dur.toHuman({ unitDisplay: "short" }) //=> '1 day, 5 hr, 6 min' + * ``` + */ + toHuman(opts = {}) { + if (!this.isValid) return INVALID; + + const l = orderedUnits + .map((unit) => { + const val = this.values[unit]; + if (isUndefined(val)) { + return null; + } + return this.loc + .numberFormatter({ style: "unit", unitDisplay: "long", ...opts, unit: unit.slice(0, -1) }) + .format(val); + }) + .filter((n) => n); + + return this.loc + .listFormatter({ type: "conjunction", style: opts.listStyle || "narrow", ...opts }) + .format(l); + } + + /** + * Returns a JavaScript object with this Duration's values. + * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toObject() //=> { years: 1, days: 6, seconds: 2 } + * @return {Object} + */ + toObject() { + if (!this.isValid) return {}; + return { ...this.values }; + } + + /** + * Returns an ISO 8601-compliant string representation of this Duration. + * @see https://en.wikipedia.org/wiki/ISO_8601#Durations + * @example Duration.fromObject({ years: 3, seconds: 45 }).toISO() //=> 'P3YT45S' + * @example Duration.fromObject({ months: 4, seconds: 45 }).toISO() //=> 'P4MT45S' + * @example Duration.fromObject({ months: 5 }).toISO() //=> 'P5M' + * @example Duration.fromObject({ minutes: 5 }).toISO() //=> 'PT5M' + * @example Duration.fromObject({ milliseconds: 6 }).toISO() //=> 'PT0.006S' + * @return {string} + */ + toISO() { + // we could use the formatter, but this is an easier way to get the minimum string + if (!this.isValid) return null; + + let s = "P"; + if (this.years !== 0) s += this.years + "Y"; + if (this.months !== 0 || this.quarters !== 0) s += this.months + this.quarters * 3 + "M"; + if (this.weeks !== 0) s += this.weeks + "W"; + if (this.days !== 0) s += this.days + "D"; + if (this.hours !== 0 || this.minutes !== 0 || this.seconds !== 0 || this.milliseconds !== 0) + s += "T"; + if (this.hours !== 0) s += this.hours + "H"; + if (this.minutes !== 0) s += this.minutes + "M"; + if (this.seconds !== 0 || this.milliseconds !== 0) + // this will handle "floating point madness" by removing extra decimal places + // https://stackoverflow.com/questions/588004/is-floating-point-math-broken + s += roundTo(this.seconds + this.milliseconds / 1000, 3) + "S"; + if (s === "P") s += "T0S"; + return s; + } + + /** + * Returns an ISO 8601-compliant string representation of this Duration, formatted as a time of day. + * Note that this will return null if the duration is invalid, negative, or equal to or greater than 24 hours. + * @see https://en.wikipedia.org/wiki/ISO_8601#Times + * @param {Object} opts - options + * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0 + * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0 + * @param {boolean} [opts.includePrefix=false] - include the `T` prefix + * @param {string} [opts.format='extended'] - choose between the basic and extended format + * @example Duration.fromObject({ hours: 11 }).toISOTime() //=> '11:00:00.000' + * @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressMilliseconds: true }) //=> '11:00:00' + * @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressSeconds: true }) //=> '11:00' + * @example Duration.fromObject({ hours: 11 }).toISOTime({ includePrefix: true }) //=> 'T11:00:00.000' + * @example Duration.fromObject({ hours: 11 }).toISOTime({ format: 'basic' }) //=> '110000.000' + * @return {string} + */ + toISOTime(opts = {}) { + if (!this.isValid) return null; + + const millis = this.toMillis(); + if (millis < 0 || millis >= 86400000) return null; + + opts = { + suppressMilliseconds: false, + suppressSeconds: false, + includePrefix: false, + format: "extended", + ...opts, + includeOffset: false, + }; + + const dateTime = DateTime.fromMillis(millis, { zone: "UTC" }); + return dateTime.toISOTime(opts); + } + + /** + * Returns an ISO 8601 representation of this Duration appropriate for use in JSON. + * @return {string} + */ + toJSON() { + return this.toISO(); + } + + /** + * Returns an ISO 8601 representation of this Duration appropriate for use in debugging. + * @return {string} + */ + toString() { + return this.toISO(); + } + + /** + * Returns a string representation of this Duration appropriate for the REPL. + * @return {string} + */ + [Symbol.for("nodejs.util.inspect.custom")]() { + if (this.isValid) { + return `Duration { values: ${JSON.stringify(this.values)} }`; + } else { + return `Duration { Invalid, reason: ${this.invalidReason} }`; + } + } + + /** + * Returns an milliseconds value of this Duration. + * @return {number} + */ + toMillis() { + if (!this.isValid) return NaN; + + return durationToMillis(this.matrix, this.values); + } + + /** + * Returns an milliseconds value of this Duration. Alias of {@link toMillis} + * @return {number} + */ + valueOf() { + return this.toMillis(); + } + + /** + * Make this Duration longer by the specified amount. Return a newly-constructed Duration. + * @param {Duration|Object|number} duration - The amount to add. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject() + * @return {Duration} + */ + plus(duration) { + if (!this.isValid) return this; + + const dur = Duration.fromDurationLike(duration), + result = {}; + + for (const k of orderedUnits) { + if (hasOwnProperty(dur.values, k) || hasOwnProperty(this.values, k)) { + result[k] = dur.get(k) + this.get(k); + } + } + + return clone(this, { values: result }, true); + } + + /** + * Make this Duration shorter by the specified amount. Return a newly-constructed Duration. + * @param {Duration|Object|number} duration - The amount to subtract. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject() + * @return {Duration} + */ + minus(duration) { + if (!this.isValid) return this; + + const dur = Duration.fromDurationLike(duration); + return this.plus(dur.negate()); + } + + /** + * Scale this Duration by the specified amount. Return a newly-constructed Duration. + * @param {function} fn - The function to apply to each unit. Arity is 1 or 2: the value of the unit and, optionally, the unit name. Must return a number. + * @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits(x => x * 2) //=> { hours: 2, minutes: 60 } + * @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits((x, u) => u === "hours" ? x * 2 : x) //=> { hours: 2, minutes: 30 } + * @return {Duration} + */ + mapUnits(fn) { + if (!this.isValid) return this; + const result = {}; + for (const k of Object.keys(this.values)) { + result[k] = asNumber(fn(this.values[k], k)); + } + return clone(this, { values: result }, true); + } + + /** + * Get the value of unit. + * @param {string} unit - a unit such as 'minute' or 'day' + * @example Duration.fromObject({years: 2, days: 3}).get('years') //=> 2 + * @example Duration.fromObject({years: 2, days: 3}).get('months') //=> 0 + * @example Duration.fromObject({years: 2, days: 3}).get('days') //=> 3 + * @return {number} + */ + get(unit) { + return this[Duration.normalizeUnit(unit)]; + } + + /** + * "Set" the values of specified units. Return a newly-constructed Duration. + * @param {Object} values - a mapping of units to numbers + * @example dur.set({ years: 2017 }) + * @example dur.set({ hours: 8, minutes: 30 }) + * @return {Duration} + */ + set(values) { + if (!this.isValid) return this; + + const mixed = { ...this.values, ...normalizeObject(values, Duration.normalizeUnit) }; + return clone(this, { values: mixed }); + } + + /** + * "Set" the locale and/or numberingSystem. Returns a newly-constructed Duration. + * @example dur.reconfigure({ locale: 'en-GB' }) + * @return {Duration} + */ + reconfigure({ locale, numberingSystem, conversionAccuracy, matrix } = {}) { + const loc = this.loc.clone({ locale, numberingSystem }); + const opts = { loc, matrix, conversionAccuracy }; + return clone(this, opts); + } + + /** + * Return the length of the duration in the specified unit. + * @param {string} unit - a unit such as 'minutes' or 'days' + * @example Duration.fromObject({years: 1}).as('days') //=> 365 + * @example Duration.fromObject({years: 1}).as('months') //=> 12 + * @example Duration.fromObject({hours: 60}).as('days') //=> 2.5 + * @return {number} + */ + as(unit) { + return this.isValid ? this.shiftTo(unit).get(unit) : NaN; + } + + /** + * Reduce this Duration to its canonical representation in its current units. + * Assuming the overall value of the Duration is positive, this means: + * - excessive values for lower-order units are converted to higher-order units (if possible, see first and second example) + * - negative lower-order units are converted to higher order units (there must be such a higher order unit, otherwise + * the overall value would be negative, see third example) + * - fractional values for higher-order units are converted to lower-order units (if possible, see fourth example) + * + * If the overall value is negative, the result of this method is equivalent to `this.negate().normalize().negate()`. + * @example Duration.fromObject({ years: 2, days: 5000 }).normalize().toObject() //=> { years: 15, days: 255 } + * @example Duration.fromObject({ days: 5000 }).normalize().toObject() //=> { days: 5000 } + * @example Duration.fromObject({ hours: 12, minutes: -45 }).normalize().toObject() //=> { hours: 11, minutes: 15 } + * @example Duration.fromObject({ years: 2.5, days: 0, hours: 0 }).normalize().toObject() //=> { years: 2, days: 182, hours: 12 } + * @return {Duration} + */ + normalize() { + if (!this.isValid) return this; + const vals = this.toObject(); + normalizeValues(this.matrix, vals); + return clone(this, { values: vals }, true); + } + + /** + * Rescale units to its largest representation + * @example Duration.fromObject({ milliseconds: 90000 }).rescale().toObject() //=> { minutes: 1, seconds: 30 } + * @return {Duration} + */ + rescale() { + if (!this.isValid) return this; + const vals = removeZeroes(this.normalize().shiftToAll().toObject()); + return clone(this, { values: vals }, true); + } + + /** + * Convert this Duration into its representation in a different set of units. + * @example Duration.fromObject({ hours: 1, seconds: 30 }).shiftTo('minutes', 'milliseconds').toObject() //=> { minutes: 60, milliseconds: 30000 } + * @return {Duration} + */ + shiftTo(...units) { + if (!this.isValid) return this; + + if (units.length === 0) { + return this; + } + + units = units.map((u) => Duration.normalizeUnit(u)); + + const built = {}, + accumulated = {}, + vals = this.toObject(); + let lastUnit; + + for (const k of orderedUnits) { + if (units.indexOf(k) >= 0) { + lastUnit = k; + + let own = 0; + + // anything we haven't boiled down yet should get boiled to this unit + for (const ak in accumulated) { + own += this.matrix[ak][k] * accumulated[ak]; + accumulated[ak] = 0; + } + + // plus anything that's already in this unit + if (isNumber(vals[k])) { + own += vals[k]; + } + + // only keep the integer part for now in the hopes of putting any decimal part + // into a smaller unit later + const i = Math.trunc(own); + built[k] = i; + accumulated[k] = (own * 1000 - i * 1000) / 1000; + + // otherwise, keep it in the wings to boil it later + } else if (isNumber(vals[k])) { + accumulated[k] = vals[k]; + } + } + + // anything leftover becomes the decimal for the last unit + // lastUnit must be defined since units is not empty + for (const key in accumulated) { + if (accumulated[key] !== 0) { + built[lastUnit] += + key === lastUnit ? accumulated[key] : accumulated[key] / this.matrix[lastUnit][key]; + } + } + + normalizeValues(this.matrix, built); + return clone(this, { values: built }, true); + } + + /** + * Shift this Duration to all available units. + * Same as shiftTo("years", "months", "weeks", "days", "hours", "minutes", "seconds", "milliseconds") + * @return {Duration} + */ + shiftToAll() { + if (!this.isValid) return this; + return this.shiftTo( + "years", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", + "milliseconds" + ); + } + + /** + * Return the negative of this Duration. + * @example Duration.fromObject({ hours: 1, seconds: 30 }).negate().toObject() //=> { hours: -1, seconds: -30 } + * @return {Duration} + */ + negate() { + if (!this.isValid) return this; + const negated = {}; + for (const k of Object.keys(this.values)) { + negated[k] = this.values[k] === 0 ? 0 : -this.values[k]; + } + return clone(this, { values: negated }, true); + } + + /** + * Get the years. + * @type {number} + */ + get years() { + return this.isValid ? this.values.years || 0 : NaN; + } + + /** + * Get the quarters. + * @type {number} + */ + get quarters() { + return this.isValid ? this.values.quarters || 0 : NaN; + } + + /** + * Get the months. + * @type {number} + */ + get months() { + return this.isValid ? this.values.months || 0 : NaN; + } + + /** + * Get the weeks + * @type {number} + */ + get weeks() { + return this.isValid ? this.values.weeks || 0 : NaN; + } + + /** + * Get the days. + * @type {number} + */ + get days() { + return this.isValid ? this.values.days || 0 : NaN; + } + + /** + * Get the hours. + * @type {number} + */ + get hours() { + return this.isValid ? this.values.hours || 0 : NaN; + } + + /** + * Get the minutes. + * @type {number} + */ + get minutes() { + return this.isValid ? this.values.minutes || 0 : NaN; + } + + /** + * Get the seconds. + * @return {number} + */ + get seconds() { + return this.isValid ? this.values.seconds || 0 : NaN; + } + + /** + * Get the milliseconds. + * @return {number} + */ + get milliseconds() { + return this.isValid ? this.values.milliseconds || 0 : NaN; + } + + /** + * Returns whether the Duration is invalid. Invalid durations are returned by diff operations + * on invalid DateTimes or Intervals. + * @return {boolean} + */ + get isValid() { + return this.invalid === null; + } + + /** + * Returns an error code if this Duration became invalid, or null if the Duration is valid + * @return {string} + */ + get invalidReason() { + return this.invalid ? this.invalid.reason : null; + } + + /** + * Returns an explanation of why this Duration became invalid, or null if the Duration is valid + * @type {string} + */ + get invalidExplanation() { + return this.invalid ? this.invalid.explanation : null; + } + + /** + * Equality check + * Two Durations are equal iff they have the same units and the same values for each unit. + * @param {Duration} other + * @return {boolean} + */ + equals(other) { + if (!this.isValid || !other.isValid) { + return false; + } + + if (!this.loc.equals(other.loc)) { + return false; + } + + function eq(v1, v2) { + // Consider 0 and undefined as equal + if (v1 === undefined || v1 === 0) return v2 === undefined || v2 === 0; + return v1 === v2; + } + + for (const u of orderedUnits) { + if (!eq(this.values[u], other.values[u])) { + return false; + } + } + return true; + } +} diff --git a/src/shared/libs/luxon/src/errors.js b/src/shared/libs/luxon/src/errors.js new file mode 100644 index 0000000..c1f363e --- /dev/null +++ b/src/shared/libs/luxon/src/errors.js @@ -0,0 +1,61 @@ +// these aren't really private, but nor are they really useful to document + +/** + * @private + */ +class LuxonError extends Error {} + +/** + * @private + */ +export class InvalidDateTimeError extends LuxonError { + constructor(reason) { + super(`Invalid DateTime: ${reason.toMessage()}`); + } +} + +/** + * @private + */ +export class InvalidIntervalError extends LuxonError { + constructor(reason) { + super(`Invalid Interval: ${reason.toMessage()}`); + } +} + +/** + * @private + */ +export class InvalidDurationError extends LuxonError { + constructor(reason) { + super(`Invalid Duration: ${reason.toMessage()}`); + } +} + +/** + * @private + */ +export class ConflictingSpecificationError extends LuxonError {} + +/** + * @private + */ +export class InvalidUnitError extends LuxonError { + constructor(unit) { + super(`Invalid unit ${unit}`); + } +} + +/** + * @private + */ +export class InvalidArgumentError extends LuxonError {} + +/** + * @private + */ +export class ZoneIsAbstractError extends LuxonError { + constructor() { + super("Zone is an abstract class"); + } +} diff --git a/src/shared/libs/luxon/src/impl/conversions.js b/src/shared/libs/luxon/src/impl/conversions.js new file mode 100644 index 0000000..4c7d117 --- /dev/null +++ b/src/shared/libs/luxon/src/impl/conversions.js @@ -0,0 +1,206 @@ +import { + integerBetween, + isLeapYear, + timeObject, + daysInYear, + daysInMonth, + weeksInWeekYear, + isInteger, + isUndefined, +} from "./util.js"; +import Invalid from "./invalid.js"; +import { ConflictingSpecificationError } from "../errors.js"; + +const nonLeapLadder = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334], + leapLadder = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]; + +function unitOutOfRange(unit, value) { + return new Invalid( + "unit out of range", + `you specified ${value} (of type ${typeof value}) as a ${unit}, which is invalid` + ); +} + +export function dayOfWeek(year, month, day) { + const d = new Date(Date.UTC(year, month - 1, day)); + + if (year < 100 && year >= 0) { + d.setUTCFullYear(d.getUTCFullYear() - 1900); + } + + const js = d.getUTCDay(); + + return js === 0 ? 7 : js; +} + +function computeOrdinal(year, month, day) { + return day + (isLeapYear(year) ? leapLadder : nonLeapLadder)[month - 1]; +} + +function uncomputeOrdinal(year, ordinal) { + const table = isLeapYear(year) ? leapLadder : nonLeapLadder, + month0 = table.findIndex((i) => i < ordinal), + day = ordinal - table[month0]; + return { month: month0 + 1, day }; +} + +export function isoWeekdayToLocal(isoWeekday, startOfWeek) { + return ((isoWeekday - startOfWeek + 7) % 7) + 1; +} + +/** + * @private + */ + +export function gregorianToWeek(gregObj, minDaysInFirstWeek = 4, startOfWeek = 1) { + const { year, month, day } = gregObj, + ordinal = computeOrdinal(year, month, day), + weekday = isoWeekdayToLocal(dayOfWeek(year, month, day), startOfWeek); + + let weekNumber = Math.floor((ordinal - weekday + 14 - minDaysInFirstWeek) / 7), + weekYear; + + if (weekNumber < 1) { + weekYear = year - 1; + weekNumber = weeksInWeekYear(weekYear, minDaysInFirstWeek, startOfWeek); + } else if (weekNumber > weeksInWeekYear(year, minDaysInFirstWeek, startOfWeek)) { + weekYear = year + 1; + weekNumber = 1; + } else { + weekYear = year; + } + + return { weekYear, weekNumber, weekday, ...timeObject(gregObj) }; +} + +export function weekToGregorian(weekData, minDaysInFirstWeek = 4, startOfWeek = 1) { + const { weekYear, weekNumber, weekday } = weekData, + weekdayOfJan4 = isoWeekdayToLocal(dayOfWeek(weekYear, 1, minDaysInFirstWeek), startOfWeek), + yearInDays = daysInYear(weekYear); + + let ordinal = weekNumber * 7 + weekday - weekdayOfJan4 - 7 + minDaysInFirstWeek, + year; + + if (ordinal < 1) { + year = weekYear - 1; + ordinal += daysInYear(year); + } else if (ordinal > yearInDays) { + year = weekYear + 1; + ordinal -= daysInYear(weekYear); + } else { + year = weekYear; + } + + const { month, day } = uncomputeOrdinal(year, ordinal); + return { year, month, day, ...timeObject(weekData) }; +} + +export function gregorianToOrdinal(gregData) { + const { year, month, day } = gregData; + const ordinal = computeOrdinal(year, month, day); + return { year, ordinal, ...timeObject(gregData) }; +} + +export function ordinalToGregorian(ordinalData) { + const { year, ordinal } = ordinalData; + const { month, day } = uncomputeOrdinal(year, ordinal); + return { year, month, day, ...timeObject(ordinalData) }; +} + +/** + * Check if local week units like localWeekday are used in obj. + * If so, validates that they are not mixed with ISO week units and then copies them to the normal week unit properties. + * Modifies obj in-place! + * @param obj the object values + */ +export function usesLocalWeekValues(obj, loc) { + const hasLocaleWeekData = + !isUndefined(obj.localWeekday) || + !isUndefined(obj.localWeekNumber) || + !isUndefined(obj.localWeekYear); + if (hasLocaleWeekData) { + const hasIsoWeekData = + !isUndefined(obj.weekday) || !isUndefined(obj.weekNumber) || !isUndefined(obj.weekYear); + + if (hasIsoWeekData) { + throw new ConflictingSpecificationError( + "Cannot mix locale-based week fields with ISO-based week fields" + ); + } + if (!isUndefined(obj.localWeekday)) obj.weekday = obj.localWeekday; + if (!isUndefined(obj.localWeekNumber)) obj.weekNumber = obj.localWeekNumber; + if (!isUndefined(obj.localWeekYear)) obj.weekYear = obj.localWeekYear; + delete obj.localWeekday; + delete obj.localWeekNumber; + delete obj.localWeekYear; + return { + minDaysInFirstWeek: loc.getMinDaysInFirstWeek(), + startOfWeek: loc.getStartOfWeek(), + }; + } else { + return { minDaysInFirstWeek: 4, startOfWeek: 1 }; + } +} + +export function hasInvalidWeekData(obj, minDaysInFirstWeek = 4, startOfWeek = 1) { + const validYear = isInteger(obj.weekYear), + validWeek = integerBetween( + obj.weekNumber, + 1, + weeksInWeekYear(obj.weekYear, minDaysInFirstWeek, startOfWeek) + ), + validWeekday = integerBetween(obj.weekday, 1, 7); + + if (!validYear) { + return unitOutOfRange("weekYear", obj.weekYear); + } else if (!validWeek) { + return unitOutOfRange("week", obj.weekNumber); + } else if (!validWeekday) { + return unitOutOfRange("weekday", obj.weekday); + } else return false; +} + +export function hasInvalidOrdinalData(obj) { + const validYear = isInteger(obj.year), + validOrdinal = integerBetween(obj.ordinal, 1, daysInYear(obj.year)); + + if (!validYear) { + return unitOutOfRange("year", obj.year); + } else if (!validOrdinal) { + return unitOutOfRange("ordinal", obj.ordinal); + } else return false; +} + +export function hasInvalidGregorianData(obj) { + const validYear = isInteger(obj.year), + validMonth = integerBetween(obj.month, 1, 12), + validDay = integerBetween(obj.day, 1, daysInMonth(obj.year, obj.month)); + + if (!validYear) { + return unitOutOfRange("year", obj.year); + } else if (!validMonth) { + return unitOutOfRange("month", obj.month); + } else if (!validDay) { + return unitOutOfRange("day", obj.day); + } else return false; +} + +export function hasInvalidTimeData(obj) { + const { hour, minute, second, millisecond } = obj; + const validHour = + integerBetween(hour, 0, 23) || + (hour === 24 && minute === 0 && second === 0 && millisecond === 0), + validMinute = integerBetween(minute, 0, 59), + validSecond = integerBetween(second, 0, 59), + validMillisecond = integerBetween(millisecond, 0, 999); + + if (!validHour) { + return unitOutOfRange("hour", hour); + } else if (!validMinute) { + return unitOutOfRange("minute", minute); + } else if (!validSecond) { + return unitOutOfRange("second", second); + } else if (!validMillisecond) { + return unitOutOfRange("millisecond", millisecond); + } else return false; +} diff --git a/src/shared/libs/luxon/src/impl/diff.js b/src/shared/libs/luxon/src/impl/diff.js new file mode 100644 index 0000000..ebd129e --- /dev/null +++ b/src/shared/libs/luxon/src/impl/diff.js @@ -0,0 +1,95 @@ +import Duration from "../duration.js"; + +function dayDiff(earlier, later) { + const utcDayStart = (dt) => dt.toUTC(0, { keepLocalTime: true }).startOf("day").valueOf(), + ms = utcDayStart(later) - utcDayStart(earlier); + return Math.floor(Duration.fromMillis(ms).as("days")); +} + +function highOrderDiffs(cursor, later, units) { + const differs = [ + ["years", (a, b) => b.year - a.year], + ["quarters", (a, b) => b.quarter - a.quarter + (b.year - a.year) * 4], + ["months", (a, b) => b.month - a.month + (b.year - a.year) * 12], + [ + "weeks", + (a, b) => { + const days = dayDiff(a, b); + return (days - (days % 7)) / 7; + }, + ], + ["days", dayDiff], + ]; + + const results = {}; + const earlier = cursor; + let lowestOrder, highWater; + + /* This loop tries to diff using larger units first. + If we overshoot, we backtrack and try the next smaller unit. + "cursor" starts out at the earlier timestamp and moves closer and closer to "later" + as we use smaller and smaller units. + highWater keeps track of where we would be if we added one more of the smallest unit, + this is used later to potentially convert any difference smaller than the smallest higher order unit + into a fraction of that smallest higher order unit + */ + for (const [unit, differ] of differs) { + if (units.indexOf(unit) >= 0) { + lowestOrder = unit; + + results[unit] = differ(cursor, later); + highWater = earlier.plus(results); + + if (highWater > later) { + // we overshot the end point, backtrack cursor by 1 + results[unit]--; + cursor = earlier.plus(results); + + // if we are still overshooting now, we need to backtrack again + // this happens in certain situations when diffing times in different zones, + // because this calculation ignores time zones + if (cursor > later) { + // keep the "overshot by 1" around as highWater + highWater = cursor; + // backtrack cursor by 1 + results[unit]--; + cursor = earlier.plus(results); + } + } else { + cursor = highWater; + } + } + } + + return [cursor, results, highWater, lowestOrder]; +} + +export default function (earlier, later, units, opts) { + let [cursor, results, highWater, lowestOrder] = highOrderDiffs(earlier, later, units); + + const remainingMillis = later - cursor; + + const lowerOrderUnits = units.filter( + (u) => ["hours", "minutes", "seconds", "milliseconds"].indexOf(u) >= 0 + ); + + if (lowerOrderUnits.length === 0) { + if (highWater < later) { + highWater = cursor.plus({ [lowestOrder]: 1 }); + } + + if (highWater !== cursor) { + results[lowestOrder] = (results[lowestOrder] || 0) + remainingMillis / (highWater - cursor); + } + } + + const duration = Duration.fromObject(results, opts); + + if (lowerOrderUnits.length > 0) { + return Duration.fromMillis(remainingMillis, opts) + .shiftTo(...lowerOrderUnits) + .plus(duration); + } else { + return duration; + } +} diff --git a/src/shared/libs/luxon/src/impl/digits.js b/src/shared/libs/luxon/src/impl/digits.js new file mode 100644 index 0000000..6ac1bae --- /dev/null +++ b/src/shared/libs/luxon/src/impl/digits.js @@ -0,0 +1,75 @@ +const numberingSystems = { + arab: "[\u0660-\u0669]", + arabext: "[\u06F0-\u06F9]", + bali: "[\u1B50-\u1B59]", + beng: "[\u09E6-\u09EF]", + deva: "[\u0966-\u096F]", + fullwide: "[\uFF10-\uFF19]", + gujr: "[\u0AE6-\u0AEF]", + hanidec: "[〇|一|二|三|四|五|六|七|八|九]", + khmr: "[\u17E0-\u17E9]", + knda: "[\u0CE6-\u0CEF]", + laoo: "[\u0ED0-\u0ED9]", + limb: "[\u1946-\u194F]", + mlym: "[\u0D66-\u0D6F]", + mong: "[\u1810-\u1819]", + mymr: "[\u1040-\u1049]", + orya: "[\u0B66-\u0B6F]", + tamldec: "[\u0BE6-\u0BEF]", + telu: "[\u0C66-\u0C6F]", + thai: "[\u0E50-\u0E59]", + tibt: "[\u0F20-\u0F29]", + latn: "\\d", +}; + +const numberingSystemsUTF16 = { + arab: [1632, 1641], + arabext: [1776, 1785], + bali: [6992, 7001], + beng: [2534, 2543], + deva: [2406, 2415], + fullwide: [65296, 65303], + gujr: [2790, 2799], + khmr: [6112, 6121], + knda: [3302, 3311], + laoo: [3792, 3801], + limb: [6470, 6479], + mlym: [3430, 3439], + mong: [6160, 6169], + mymr: [4160, 4169], + orya: [2918, 2927], + tamldec: [3046, 3055], + telu: [3174, 3183], + thai: [3664, 3673], + tibt: [3872, 3881], +}; + +const hanidecChars = numberingSystems.hanidec.replace(/[\[|\]]/g, "").split(""); + +export function parseDigits(str) { + let value = parseInt(str, 10); + if (isNaN(value)) { + value = ""; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + + if (str[i].search(numberingSystems.hanidec) !== -1) { + value += hanidecChars.indexOf(str[i]); + } else { + for (const key in numberingSystemsUTF16) { + const [min, max] = numberingSystemsUTF16[key]; + if (code >= min && code <= max) { + value += code - min; + } + } + } + } + return parseInt(value, 10); + } else { + return value; + } +} + +export function digitRegex({ numberingSystem }, append = "") { + return new RegExp(`${numberingSystems[numberingSystem || "latn"]}${append}`); +} diff --git a/src/shared/libs/luxon/src/impl/english.js b/src/shared/libs/luxon/src/impl/english.js new file mode 100644 index 0000000..cf93798 --- /dev/null +++ b/src/shared/libs/luxon/src/impl/english.js @@ -0,0 +1,233 @@ +import * as Formats from "./formats.js"; +import { pick } from "./util.js"; + +function stringify(obj) { + return JSON.stringify(obj, Object.keys(obj).sort()); +} + +/** + * @private + */ + +export const monthsLong = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +export const monthsShort = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +]; + +export const monthsNarrow = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]; + +export function months(length) { + switch (length) { + case "narrow": + return [...monthsNarrow]; + case "short": + return [...monthsShort]; + case "long": + return [...monthsLong]; + case "numeric": + return ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]; + case "2-digit": + return ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; + default: + return null; + } +} + +export const weekdaysLong = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +]; + +export const weekdaysShort = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + +export const weekdaysNarrow = ["M", "T", "W", "T", "F", "S", "S"]; + +export function weekdays(length) { + switch (length) { + case "narrow": + return [...weekdaysNarrow]; + case "short": + return [...weekdaysShort]; + case "long": + return [...weekdaysLong]; + case "numeric": + return ["1", "2", "3", "4", "5", "6", "7"]; + default: + return null; + } +} + +export const meridiems = ["AM", "PM"]; + +export const erasLong = ["Before Christ", "Anno Domini"]; + +export const erasShort = ["BC", "AD"]; + +export const erasNarrow = ["B", "A"]; + +export function eras(length) { + switch (length) { + case "narrow": + return [...erasNarrow]; + case "short": + return [...erasShort]; + case "long": + return [...erasLong]; + default: + return null; + } +} + +export function meridiemForDateTime(dt) { + return meridiems[dt.hour < 12 ? 0 : 1]; +} + +export function weekdayForDateTime(dt, length) { + return weekdays(length)[dt.weekday - 1]; +} + +export function monthForDateTime(dt, length) { + return months(length)[dt.month - 1]; +} + +export function eraForDateTime(dt, length) { + return eras(length)[dt.year < 0 ? 0 : 1]; +} + +export function formatRelativeTime(unit, count, numeric = "always", narrow = false) { + const units = { + years: ["year", "yr."], + quarters: ["quarter", "qtr."], + months: ["month", "mo."], + weeks: ["week", "wk."], + days: ["day", "day", "days"], + hours: ["hour", "hr."], + minutes: ["minute", "min."], + seconds: ["second", "sec."], + }; + + const lastable = ["hours", "minutes", "seconds"].indexOf(unit) === -1; + + if (numeric === "auto" && lastable) { + const isDay = unit === "days"; + switch (count) { + case 1: + return isDay ? "tomorrow" : `next ${units[unit][0]}`; + case -1: + return isDay ? "yesterday" : `last ${units[unit][0]}`; + case 0: + return isDay ? "today" : `this ${units[unit][0]}`; + default: // fall through + } + } + + const isInPast = Object.is(count, -0) || count < 0, + fmtValue = Math.abs(count), + singular = fmtValue === 1, + lilUnits = units[unit], + fmtUnit = narrow + ? singular + ? lilUnits[1] + : lilUnits[2] || lilUnits[1] + : singular + ? units[unit][0] + : unit; + return isInPast ? `${fmtValue} ${fmtUnit} ago` : `in ${fmtValue} ${fmtUnit}`; +} + +export function formatString(knownFormat) { + // these all have the offsets removed because we don't have access to them + // without all the intl stuff this is backfilling + const filtered = pick(knownFormat, [ + "weekday", + "era", + "year", + "month", + "day", + "hour", + "minute", + "second", + "timeZoneName", + "hourCycle", + ]), + key = stringify(filtered), + dateTimeHuge = "EEEE, LLLL d, yyyy, h:mm a"; + switch (key) { + case stringify(Formats.DATE_SHORT): + return "M/d/yyyy"; + case stringify(Formats.DATE_MED): + return "LLL d, yyyy"; + case stringify(Formats.DATE_MED_WITH_WEEKDAY): + return "EEE, LLL d, yyyy"; + case stringify(Formats.DATE_FULL): + return "LLLL d, yyyy"; + case stringify(Formats.DATE_HUGE): + return "EEEE, LLLL d, yyyy"; + case stringify(Formats.TIME_SIMPLE): + return "h:mm a"; + case stringify(Formats.TIME_WITH_SECONDS): + return "h:mm:ss a"; + case stringify(Formats.TIME_WITH_SHORT_OFFSET): + return "h:mm a"; + case stringify(Formats.TIME_WITH_LONG_OFFSET): + return "h:mm a"; + case stringify(Formats.TIME_24_SIMPLE): + return "HH:mm"; + case stringify(Formats.TIME_24_WITH_SECONDS): + return "HH:mm:ss"; + case stringify(Formats.TIME_24_WITH_SHORT_OFFSET): + return "HH:mm"; + case stringify(Formats.TIME_24_WITH_LONG_OFFSET): + return "HH:mm"; + case stringify(Formats.DATETIME_SHORT): + return "M/d/yyyy, h:mm a"; + case stringify(Formats.DATETIME_MED): + return "LLL d, yyyy, h:mm a"; + case stringify(Formats.DATETIME_FULL): + return "LLLL d, yyyy, h:mm a"; + case stringify(Formats.DATETIME_HUGE): + return dateTimeHuge; + case stringify(Formats.DATETIME_SHORT_WITH_SECONDS): + return "M/d/yyyy, h:mm:ss a"; + case stringify(Formats.DATETIME_MED_WITH_SECONDS): + return "LLL d, yyyy, h:mm:ss a"; + case stringify(Formats.DATETIME_MED_WITH_WEEKDAY): + return "EEE, d LLL yyyy, h:mm a"; + case stringify(Formats.DATETIME_FULL_WITH_SECONDS): + return "LLLL d, yyyy, h:mm:ss a"; + case stringify(Formats.DATETIME_HUGE_WITH_SECONDS): + return "EEEE, LLLL d, yyyy, h:mm:ss a"; + default: + return dateTimeHuge; + } +} diff --git a/src/shared/libs/luxon/src/impl/formats.js b/src/shared/libs/luxon/src/impl/formats.js new file mode 100644 index 0000000..f2efaad --- /dev/null +++ b/src/shared/libs/luxon/src/impl/formats.js @@ -0,0 +1,176 @@ +/** + * @private + */ + +const n = "numeric", + s = "short", + l = "long"; + +export const DATE_SHORT = { + year: n, + month: n, + day: n, +}; + +export const DATE_MED = { + year: n, + month: s, + day: n, +}; + +export const DATE_MED_WITH_WEEKDAY = { + year: n, + month: s, + day: n, + weekday: s, +}; + +export const DATE_FULL = { + year: n, + month: l, + day: n, +}; + +export const DATE_HUGE = { + year: n, + month: l, + day: n, + weekday: l, +}; + +export const TIME_SIMPLE = { + hour: n, + minute: n, +}; + +export const TIME_WITH_SECONDS = { + hour: n, + minute: n, + second: n, +}; + +export const TIME_WITH_SHORT_OFFSET = { + hour: n, + minute: n, + second: n, + timeZoneName: s, +}; + +export const TIME_WITH_LONG_OFFSET = { + hour: n, + minute: n, + second: n, + timeZoneName: l, +}; + +export const TIME_24_SIMPLE = { + hour: n, + minute: n, + hourCycle: "h23", +}; + +export const TIME_24_WITH_SECONDS = { + hour: n, + minute: n, + second: n, + hourCycle: "h23", +}; + +export const TIME_24_WITH_SHORT_OFFSET = { + hour: n, + minute: n, + second: n, + hourCycle: "h23", + timeZoneName: s, +}; + +export const TIME_24_WITH_LONG_OFFSET = { + hour: n, + minute: n, + second: n, + hourCycle: "h23", + timeZoneName: l, +}; + +export const DATETIME_SHORT = { + year: n, + month: n, + day: n, + hour: n, + minute: n, +}; + +export const DATETIME_SHORT_WITH_SECONDS = { + year: n, + month: n, + day: n, + hour: n, + minute: n, + second: n, +}; + +export const DATETIME_MED = { + year: n, + month: s, + day: n, + hour: n, + minute: n, +}; + +export const DATETIME_MED_WITH_SECONDS = { + year: n, + month: s, + day: n, + hour: n, + minute: n, + second: n, +}; + +export const DATETIME_MED_WITH_WEEKDAY = { + year: n, + month: s, + day: n, + weekday: s, + hour: n, + minute: n, +}; + +export const DATETIME_FULL = { + year: n, + month: l, + day: n, + hour: n, + minute: n, + timeZoneName: s, +}; + +export const DATETIME_FULL_WITH_SECONDS = { + year: n, + month: l, + day: n, + hour: n, + minute: n, + second: n, + timeZoneName: s, +}; + +export const DATETIME_HUGE = { + year: n, + month: l, + day: n, + weekday: l, + hour: n, + minute: n, + timeZoneName: l, +}; + +export const DATETIME_HUGE_WITH_SECONDS = { + year: n, + month: l, + day: n, + weekday: l, + hour: n, + minute: n, + second: n, + timeZoneName: l, +}; diff --git a/src/shared/libs/luxon/src/impl/formatter.js b/src/shared/libs/luxon/src/impl/formatter.js new file mode 100644 index 0000000..eb0bdb6 --- /dev/null +++ b/src/shared/libs/luxon/src/impl/formatter.js @@ -0,0 +1,409 @@ +import * as English from "./english.js"; +import * as Formats from "./formats.js"; +import { padStart } from "./util.js"; + +function stringifyTokens(splits, tokenToString) { + let s = ""; + for (const token of splits) { + if (token.literal) { + s += token.val; + } else { + s += tokenToString(token.val); + } + } + return s; +} + +const macroTokenToFormatOpts = { + D: Formats.DATE_SHORT, + DD: Formats.DATE_MED, + DDD: Formats.DATE_FULL, + DDDD: Formats.DATE_HUGE, + t: Formats.TIME_SIMPLE, + tt: Formats.TIME_WITH_SECONDS, + ttt: Formats.TIME_WITH_SHORT_OFFSET, + tttt: Formats.TIME_WITH_LONG_OFFSET, + T: Formats.TIME_24_SIMPLE, + TT: Formats.TIME_24_WITH_SECONDS, + TTT: Formats.TIME_24_WITH_SHORT_OFFSET, + TTTT: Formats.TIME_24_WITH_LONG_OFFSET, + f: Formats.DATETIME_SHORT, + ff: Formats.DATETIME_MED, + fff: Formats.DATETIME_FULL, + ffff: Formats.DATETIME_HUGE, + F: Formats.DATETIME_SHORT_WITH_SECONDS, + FF: Formats.DATETIME_MED_WITH_SECONDS, + FFF: Formats.DATETIME_FULL_WITH_SECONDS, + FFFF: Formats.DATETIME_HUGE_WITH_SECONDS, +}; + +/** + * @private + */ + +export default class Formatter { + static create(locale, opts = {}) { + return new Formatter(locale, opts); + } + + static parseFormat(fmt) { + // white-space is always considered a literal in user-provided formats + // the " " token has a special meaning (see unitForToken) + + let current = null, + currentFull = "", + bracketed = false; + const splits = []; + for (let i = 0; i < fmt.length; i++) { + const c = fmt.charAt(i); + if (c === "'") { + if (currentFull.length > 0) { + splits.push({ literal: bracketed || /^\s+$/.test(currentFull), val: currentFull }); + } + current = null; + currentFull = ""; + bracketed = !bracketed; + } else if (bracketed) { + currentFull += c; + } else if (c === current) { + currentFull += c; + } else { + if (currentFull.length > 0) { + splits.push({ literal: /^\s+$/.test(currentFull), val: currentFull }); + } + currentFull = c; + current = c; + } + } + + if (currentFull.length > 0) { + splits.push({ literal: bracketed || /^\s+$/.test(currentFull), val: currentFull }); + } + + return splits; + } + + static macroTokenToFormatOpts(token) { + return macroTokenToFormatOpts[token]; + } + + constructor(locale, formatOpts) { + this.opts = formatOpts; + this.loc = locale; + this.systemLoc = null; + } + + formatWithSystemDefault(dt, opts) { + if (this.systemLoc === null) { + this.systemLoc = this.loc.redefaultToSystem(); + } + const df = this.systemLoc.dtFormatter(dt, { ...this.opts, ...opts }); + return df.format(); + } + + dtFormatter(dt, opts = {}) { + return this.loc.dtFormatter(dt, { ...this.opts, ...opts }); + } + + formatDateTime(dt, opts) { + return this.dtFormatter(dt, opts).format(); + } + + formatDateTimeParts(dt, opts) { + return this.dtFormatter(dt, opts).formatToParts(); + } + + formatInterval(interval, opts) { + const df = this.dtFormatter(interval.start, opts); + return df.dtf.formatRange(interval.start.toJSDate(), interval.end.toJSDate()); + } + + resolvedOptions(dt, opts) { + return this.dtFormatter(dt, opts).resolvedOptions(); + } + + num(n, p = 0) { + // we get some perf out of doing this here, annoyingly + if (this.opts.forceSimple) { + return padStart(n, p); + } + + const opts = { ...this.opts }; + + if (p > 0) { + opts.padTo = p; + } + + return this.loc.numberFormatter(opts).format(n); + } + + formatDateTimeFromString(dt, fmt) { + const knownEnglish = this.loc.listingMode() === "en", + useDateTimeFormatter = this.loc.outputCalendar && this.loc.outputCalendar !== "gregory", + string = (opts, extract) => this.loc.extract(dt, opts, extract), + formatOffset = (opts) => { + if (dt.isOffsetFixed && dt.offset === 0 && opts.allowZ) { + return "Z"; + } + + return dt.isValid ? dt.zone.formatOffset(dt.ts, opts.format) : ""; + }, + meridiem = () => + knownEnglish + ? English.meridiemForDateTime(dt) + : string({ hour: "numeric", hourCycle: "h12" }, "dayperiod"), + month = (length, standalone) => + knownEnglish + ? English.monthForDateTime(dt, length) + : string(standalone ? { month: length } : { month: length, day: "numeric" }, "month"), + weekday = (length, standalone) => + knownEnglish + ? English.weekdayForDateTime(dt, length) + : string( + standalone ? { weekday: length } : { weekday: length, month: "long", day: "numeric" }, + "weekday" + ), + maybeMacro = (token) => { + const formatOpts = Formatter.macroTokenToFormatOpts(token); + if (formatOpts) { + return this.formatWithSystemDefault(dt, formatOpts); + } else { + return token; + } + }, + era = (length) => + knownEnglish ? English.eraForDateTime(dt, length) : string({ era: length }, "era"), + tokenToString = (token) => { + // Where possible: https://cldr.unicode.org/translation/date-time/date-time-symbols + switch (token) { + // ms + case "S": + return this.num(dt.millisecond); + case "u": + // falls through + case "SSS": + return this.num(dt.millisecond, 3); + // seconds + case "s": + return this.num(dt.second); + case "ss": + return this.num(dt.second, 2); + // fractional seconds + case "uu": + return this.num(Math.floor(dt.millisecond / 10), 2); + case "uuu": + return this.num(Math.floor(dt.millisecond / 100)); + // minutes + case "m": + return this.num(dt.minute); + case "mm": + return this.num(dt.minute, 2); + // hours + case "h": + return this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12); + case "hh": + return this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12, 2); + case "H": + return this.num(dt.hour); + case "HH": + return this.num(dt.hour, 2); + // offset + case "Z": + // like +6 + return formatOffset({ format: "narrow", allowZ: this.opts.allowZ }); + case "ZZ": + // like +06:00 + return formatOffset({ format: "short", allowZ: this.opts.allowZ }); + case "ZZZ": + // like +0600 + return formatOffset({ format: "techie", allowZ: this.opts.allowZ }); + case "ZZZZ": + // like EST + return dt.zone.offsetName(dt.ts, { format: "short", locale: this.loc.locale }); + case "ZZZZZ": + // like Eastern Standard Time + return dt.zone.offsetName(dt.ts, { format: "long", locale: this.loc.locale }); + // zone + case "z": + // like America/New_York + return dt.zoneName; + // meridiems + case "a": + return meridiem(); + // dates + case "d": + return useDateTimeFormatter ? string({ day: "numeric" }, "day") : this.num(dt.day); + case "dd": + return useDateTimeFormatter ? string({ day: "2-digit" }, "day") : this.num(dt.day, 2); + // weekdays - standalone + case "c": + // like 1 + return this.num(dt.weekday); + case "ccc": + // like 'Tues' + return weekday("short", true); + case "cccc": + // like 'Tuesday' + return weekday("long", true); + case "ccccc": + // like 'T' + return weekday("narrow", true); + // weekdays - format + case "E": + // like 1 + return this.num(dt.weekday); + case "EEE": + // like 'Tues' + return weekday("short", false); + case "EEEE": + // like 'Tuesday' + return weekday("long", false); + case "EEEEE": + // like 'T' + return weekday("narrow", false); + // months - standalone + case "L": + // like 1 + return useDateTimeFormatter + ? string({ month: "numeric", day: "numeric" }, "month") + : this.num(dt.month); + case "LL": + // like 01, doesn't seem to work + return useDateTimeFormatter + ? string({ month: "2-digit", day: "numeric" }, "month") + : this.num(dt.month, 2); + case "LLL": + // like Jan + return month("short", true); + case "LLLL": + // like January + return month("long", true); + case "LLLLL": + // like J + return month("narrow", true); + // months - format + case "M": + // like 1 + return useDateTimeFormatter + ? string({ month: "numeric" }, "month") + : this.num(dt.month); + case "MM": + // like 01 + return useDateTimeFormatter + ? string({ month: "2-digit" }, "month") + : this.num(dt.month, 2); + case "MMM": + // like Jan + return month("short", false); + case "MMMM": + // like January + return month("long", false); + case "MMMMM": + // like J + return month("narrow", false); + // years + case "y": + // like 2014 + return useDateTimeFormatter ? string({ year: "numeric" }, "year") : this.num(dt.year); + case "yy": + // like 14 + return useDateTimeFormatter + ? string({ year: "2-digit" }, "year") + : this.num(dt.year.toString().slice(-2), 2); + case "yyyy": + // like 0012 + return useDateTimeFormatter + ? string({ year: "numeric" }, "year") + : this.num(dt.year, 4); + case "yyyyyy": + // like 000012 + return useDateTimeFormatter + ? string({ year: "numeric" }, "year") + : this.num(dt.year, 6); + // eras + case "G": + // like AD + return era("short"); + case "GG": + // like Anno Domini + return era("long"); + case "GGGGG": + return era("narrow"); + case "kk": + return this.num(dt.weekYear.toString().slice(-2), 2); + case "kkkk": + return this.num(dt.weekYear, 4); + case "W": + return this.num(dt.weekNumber); + case "WW": + return this.num(dt.weekNumber, 2); + case "n": + return this.num(dt.localWeekNumber); + case "nn": + return this.num(dt.localWeekNumber, 2); + case "ii": + return this.num(dt.localWeekYear.toString().slice(-2), 2); + case "iiii": + return this.num(dt.localWeekYear, 4); + case "o": + return this.num(dt.ordinal); + case "ooo": + return this.num(dt.ordinal, 3); + case "q": + // like 1 + return this.num(dt.quarter); + case "qq": + // like 01 + return this.num(dt.quarter, 2); + case "X": + return this.num(Math.floor(dt.ts / 1000)); + case "x": + return this.num(dt.ts); + default: + return maybeMacro(token); + } + }; + + return stringifyTokens(Formatter.parseFormat(fmt), tokenToString); + } + + formatDurationFromString(dur, fmt) { + const tokenToField = (token) => { + switch (token[0]) { + case "S": + return "millisecond"; + case "s": + return "second"; + case "m": + return "minute"; + case "h": + return "hour"; + case "d": + return "day"; + case "w": + return "week"; + case "M": + return "month"; + case "y": + return "year"; + default: + return null; + } + }, + tokenToString = (lildur) => (token) => { + const mapped = tokenToField(token); + if (mapped) { + return this.num(lildur.get(mapped), token.length); + } else { + return token; + } + }, + tokens = Formatter.parseFormat(fmt), + realTokens = tokens.reduce( + (found, { literal, val }) => (literal ? found : found.concat(val)), + [] + ), + collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t) => t)); + return stringifyTokens(tokens, tokenToString(collapsed)); + } +} diff --git a/src/shared/libs/luxon/src/impl/invalid.js b/src/shared/libs/luxon/src/impl/invalid.js new file mode 100644 index 0000000..2a2c95b --- /dev/null +++ b/src/shared/libs/luxon/src/impl/invalid.js @@ -0,0 +1,14 @@ +export default class Invalid { + constructor(reason, explanation) { + this.reason = reason; + this.explanation = explanation; + } + + toMessage() { + if (this.explanation) { + return `${this.reason}: ${this.explanation}`; + } else { + return this.reason; + } + } +} diff --git a/src/shared/libs/luxon/src/impl/locale.js b/src/shared/libs/luxon/src/impl/locale.js new file mode 100644 index 0000000..f1caf14 --- /dev/null +++ b/src/shared/libs/luxon/src/impl/locale.js @@ -0,0 +1,542 @@ +import { hasLocaleWeekInfo, hasRelative, padStart, roundTo, validateWeekSettings } from "./util.js"; +import * as English from "./english.js"; +import Settings from "../settings.js"; +import DateTime from "../datetime.js"; +import IANAZone from "../zones/IANAZone.js"; + +// todo - remap caching + +let intlLFCache = {}; +function getCachedLF(locString, opts = {}) { + const key = JSON.stringify([locString, opts]); + let dtf = intlLFCache[key]; + if (!dtf) { + dtf = new Intl.ListFormat(locString, opts); + intlLFCache[key] = dtf; + } + return dtf; +} + +let intlDTCache = {}; +function getCachedDTF(locString, opts = {}) { + const key = JSON.stringify([locString, opts]); + let dtf = intlDTCache[key]; + if (!dtf) { + dtf = new Intl.DateTimeFormat(locString, opts); + intlDTCache[key] = dtf; + } + return dtf; +} + +let intlNumCache = {}; +function getCachedINF(locString, opts = {}) { + const key = JSON.stringify([locString, opts]); + let inf = intlNumCache[key]; + if (!inf) { + inf = new Intl.NumberFormat(locString, opts); + intlNumCache[key] = inf; + } + return inf; +} + +let intlRelCache = {}; +function getCachedRTF(locString, opts = {}) { + const { base, ...cacheKeyOpts } = opts; // exclude `base` from the options + const key = JSON.stringify([locString, cacheKeyOpts]); + let inf = intlRelCache[key]; + if (!inf) { + inf = new Intl.RelativeTimeFormat(locString, opts); + intlRelCache[key] = inf; + } + return inf; +} + +let sysLocaleCache = null; +function systemLocale() { + if (sysLocaleCache) { + return sysLocaleCache; + } else { + sysLocaleCache = new Intl.DateTimeFormat().resolvedOptions().locale; + return sysLocaleCache; + } +} + +let weekInfoCache = {}; +function getCachedWeekInfo(locString) { + let data = weekInfoCache[locString]; + if (!data) { + const locale = new Intl.Locale(locString); + // browsers currently implement this as a property, but spec says it should be a getter function + data = "getWeekInfo" in locale ? locale.getWeekInfo() : locale.weekInfo; + weekInfoCache[locString] = data; + } + return data; +} + +function parseLocaleString(localeStr) { + // I really want to avoid writing a BCP 47 parser + // see, e.g. https://github.com/wooorm/bcp-47 + // Instead, we'll do this: + + // a) if the string has no -u extensions, just leave it alone + // b) if it does, use Intl to resolve everything + // c) if Intl fails, try again without the -u + + // private subtags and unicode subtags have ordering requirements, + // and we're not properly parsing this, so just strip out the + // private ones if they exist. + const xIndex = localeStr.indexOf("-x-"); + if (xIndex !== -1) { + localeStr = localeStr.substring(0, xIndex); + } + + const uIndex = localeStr.indexOf("-u-"); + if (uIndex === -1) { + return [localeStr]; + } else { + let options; + let selectedStr; + try { + options = getCachedDTF(localeStr).resolvedOptions(); + selectedStr = localeStr; + } catch (e) { + const smaller = localeStr.substring(0, uIndex); + options = getCachedDTF(smaller).resolvedOptions(); + selectedStr = smaller; + } + + const { numberingSystem, calendar } = options; + return [selectedStr, numberingSystem, calendar]; + } +} + +function intlConfigString(localeStr, numberingSystem, outputCalendar) { + if (outputCalendar || numberingSystem) { + if (!localeStr.includes("-u-")) { + localeStr += "-u"; + } + + if (outputCalendar) { + localeStr += `-ca-${outputCalendar}`; + } + + if (numberingSystem) { + localeStr += `-nu-${numberingSystem}`; + } + return localeStr; + } else { + return localeStr; + } +} + +function mapMonths(f) { + const ms = []; + for (let i = 1; i <= 12; i++) { + const dt = DateTime.utc(2009, i, 1); + ms.push(f(dt)); + } + return ms; +} + +function mapWeekdays(f) { + const ms = []; + for (let i = 1; i <= 7; i++) { + const dt = DateTime.utc(2016, 11, 13 + i); + ms.push(f(dt)); + } + return ms; +} + +function listStuff(loc, length, englishFn, intlFn) { + const mode = loc.listingMode(); + + if (mode === "error") { + return null; + } else if (mode === "en") { + return englishFn(length); + } else { + return intlFn(length); + } +} + +function supportsFastNumbers(loc) { + if (loc.numberingSystem && loc.numberingSystem !== "latn") { + return false; + } else { + return ( + loc.numberingSystem === "latn" || + !loc.locale || + loc.locale.startsWith("en") || + new Intl.DateTimeFormat(loc.intl).resolvedOptions().numberingSystem === "latn" + ); + } +} + +/** + * @private + */ + +class PolyNumberFormatter { + constructor(intl, forceSimple, opts) { + this.padTo = opts.padTo || 0; + this.floor = opts.floor || false; + + const { padTo, floor, ...otherOpts } = opts; + + if (!forceSimple || Object.keys(otherOpts).length > 0) { + const intlOpts = { useGrouping: false, ...opts }; + if (opts.padTo > 0) intlOpts.minimumIntegerDigits = opts.padTo; + this.inf = getCachedINF(intl, intlOpts); + } + } + + format(i) { + if (this.inf) { + const fixed = this.floor ? Math.floor(i) : i; + return this.inf.format(fixed); + } else { + // to match the browser's numberformatter defaults + const fixed = this.floor ? Math.floor(i) : roundTo(i, 3); + return padStart(fixed, this.padTo); + } + } +} + +/** + * @private + */ + +class PolyDateFormatter { + constructor(dt, intl, opts) { + this.opts = opts; + this.originalZone = undefined; + + let z = undefined; + if (this.opts.timeZone) { + // Don't apply any workarounds if a timeZone is explicitly provided in opts + this.dt = dt; + } else if (dt.zone.type === "fixed") { + // UTC-8 or Etc/UTC-8 are not part of tzdata, only Etc/GMT+8 and the like. + // That is why fixed-offset TZ is set to that unless it is: + // 1. Representing offset 0 when UTC is used to maintain previous behavior and does not become GMT. + // 2. Unsupported by the browser: + // - some do not support Etc/ + // - < Etc/GMT-14, > Etc/GMT+12, and 30-minute or 45-minute offsets are not part of tzdata + const gmtOffset = -1 * (dt.offset / 60); + const offsetZ = gmtOffset >= 0 ? `Etc/GMT+${gmtOffset}` : `Etc/GMT${gmtOffset}`; + if (dt.offset !== 0 && IANAZone.create(offsetZ).valid) { + z = offsetZ; + this.dt = dt; + } else { + // Not all fixed-offset zones like Etc/+4:30 are present in tzdata so + // we manually apply the offset and substitute the zone as needed. + z = "UTC"; + this.dt = dt.offset === 0 ? dt : dt.setZone("UTC").plus({ minutes: dt.offset }); + this.originalZone = dt.zone; + } + } else if (dt.zone.type === "system") { + this.dt = dt; + } else if (dt.zone.type === "iana") { + this.dt = dt; + z = dt.zone.name; + } else { + // Custom zones can have any offset / offsetName so we just manually + // apply the offset and substitute the zone as needed. + z = "UTC"; + this.dt = dt.setZone("UTC").plus({ minutes: dt.offset }); + this.originalZone = dt.zone; + } + + const intlOpts = { ...this.opts }; + intlOpts.timeZone = intlOpts.timeZone || z; + this.dtf = getCachedDTF(intl, intlOpts); + } + + format() { + if (this.originalZone) { + // If we have to substitute in the actual zone name, we have to use + // formatToParts so that the timezone can be replaced. + return this.formatToParts() + .map(({ value }) => value) + .join(""); + } + return this.dtf.format(this.dt.toJSDate()); + } + + formatToParts() { + const parts = this.dtf.formatToParts(this.dt.toJSDate()); + if (this.originalZone) { + return parts.map((part) => { + if (part.type === "timeZoneName") { + const offsetName = this.originalZone.offsetName(this.dt.ts, { + locale: this.dt.locale, + format: this.opts.timeZoneName, + }); + return { + ...part, + value: offsetName, + }; + } else { + return part; + } + }); + } + return parts; + } + + resolvedOptions() { + return this.dtf.resolvedOptions(); + } +} + +/** + * @private + */ +class PolyRelFormatter { + constructor(intl, isEnglish, opts) { + this.opts = { style: "long", ...opts }; + if (!isEnglish && hasRelative()) { + this.rtf = getCachedRTF(intl, opts); + } + } + + format(count, unit) { + if (this.rtf) { + return this.rtf.format(count, unit); + } else { + return English.formatRelativeTime(unit, count, this.opts.numeric, this.opts.style !== "long"); + } + } + + formatToParts(count, unit) { + if (this.rtf) { + return this.rtf.formatToParts(count, unit); + } else { + return []; + } + } +} + +const fallbackWeekSettings = { + firstDay: 1, + minimalDays: 4, + weekend: [6, 7], +}; + +/** + * @private + */ + +export default class Locale { + static fromOpts(opts) { + return Locale.create( + opts.locale, + opts.numberingSystem, + opts.outputCalendar, + opts.weekSettings, + opts.defaultToEN + ); + } + + static create(locale, numberingSystem, outputCalendar, weekSettings, defaultToEN = false) { + const specifiedLocale = locale || Settings.defaultLocale; + // the system locale is useful for human readable strings but annoying for parsing/formatting known formats + const localeR = specifiedLocale || (defaultToEN ? "en-US" : systemLocale()); + const numberingSystemR = numberingSystem || Settings.defaultNumberingSystem; + const outputCalendarR = outputCalendar || Settings.defaultOutputCalendar; + const weekSettingsR = validateWeekSettings(weekSettings) || Settings.defaultWeekSettings; + return new Locale(localeR, numberingSystemR, outputCalendarR, weekSettingsR, specifiedLocale); + } + + static resetCache() { + sysLocaleCache = null; + intlDTCache = {}; + intlNumCache = {}; + intlRelCache = {}; + } + + static fromObject({ locale, numberingSystem, outputCalendar, weekSettings } = {}) { + return Locale.create(locale, numberingSystem, outputCalendar, weekSettings); + } + + constructor(locale, numbering, outputCalendar, weekSettings, specifiedLocale) { + const [parsedLocale, parsedNumberingSystem, parsedOutputCalendar] = parseLocaleString(locale); + + this.locale = parsedLocale; + this.numberingSystem = numbering || parsedNumberingSystem || null; + this.outputCalendar = outputCalendar || parsedOutputCalendar || null; + this.weekSettings = weekSettings; + this.intl = intlConfigString(this.locale, this.numberingSystem, this.outputCalendar); + + this.weekdaysCache = { format: {}, standalone: {} }; + this.monthsCache = { format: {}, standalone: {} }; + this.meridiemCache = null; + this.eraCache = {}; + + this.specifiedLocale = specifiedLocale; + this.fastNumbersCached = null; + } + + get fastNumbers() { + if (this.fastNumbersCached == null) { + this.fastNumbersCached = supportsFastNumbers(this); + } + + return this.fastNumbersCached; + } + + listingMode() { + const isActuallyEn = this.isEnglish(); + const hasNoWeirdness = + (this.numberingSystem === null || this.numberingSystem === "latn") && + (this.outputCalendar === null || this.outputCalendar === "gregory"); + return isActuallyEn && hasNoWeirdness ? "en" : "intl"; + } + + clone(alts) { + if (!alts || Object.getOwnPropertyNames(alts).length === 0) { + return this; + } else { + return Locale.create( + alts.locale || this.specifiedLocale, + alts.numberingSystem || this.numberingSystem, + alts.outputCalendar || this.outputCalendar, + validateWeekSettings(alts.weekSettings) || this.weekSettings, + alts.defaultToEN || false + ); + } + } + + redefaultToEN(alts = {}) { + return this.clone({ ...alts, defaultToEN: true }); + } + + redefaultToSystem(alts = {}) { + return this.clone({ ...alts, defaultToEN: false }); + } + + months(length, format = false) { + return listStuff(this, length, English.months, () => { + const intl = format ? { month: length, day: "numeric" } : { month: length }, + formatStr = format ? "format" : "standalone"; + if (!this.monthsCache[formatStr][length]) { + this.monthsCache[formatStr][length] = mapMonths((dt) => this.extract(dt, intl, "month")); + } + return this.monthsCache[formatStr][length]; + }); + } + + weekdays(length, format = false) { + return listStuff(this, length, English.weekdays, () => { + const intl = format + ? { weekday: length, year: "numeric", month: "long", day: "numeric" } + : { weekday: length }, + formatStr = format ? "format" : "standalone"; + if (!this.weekdaysCache[formatStr][length]) { + this.weekdaysCache[formatStr][length] = mapWeekdays((dt) => + this.extract(dt, intl, "weekday") + ); + } + return this.weekdaysCache[formatStr][length]; + }); + } + + meridiems() { + return listStuff( + this, + undefined, + () => English.meridiems, + () => { + // In theory there could be aribitrary day periods. We're gonna assume there are exactly two + // for AM and PM. This is probably wrong, but it's makes parsing way easier. + if (!this.meridiemCache) { + const intl = { hour: "numeric", hourCycle: "h12" }; + this.meridiemCache = [DateTime.utc(2016, 11, 13, 9), DateTime.utc(2016, 11, 13, 19)].map( + (dt) => this.extract(dt, intl, "dayperiod") + ); + } + + return this.meridiemCache; + } + ); + } + + eras(length) { + return listStuff(this, length, English.eras, () => { + const intl = { era: length }; + + // This is problematic. Different calendars are going to define eras totally differently. What I need is the minimum set of dates + // to definitely enumerate them. + if (!this.eraCache[length]) { + this.eraCache[length] = [DateTime.utc(-40, 1, 1), DateTime.utc(2017, 1, 1)].map((dt) => + this.extract(dt, intl, "era") + ); + } + + return this.eraCache[length]; + }); + } + + extract(dt, intlOpts, field) { + const df = this.dtFormatter(dt, intlOpts), + results = df.formatToParts(), + matching = results.find((m) => m.type.toLowerCase() === field); + return matching ? matching.value : null; + } + + numberFormatter(opts = {}) { + // this forcesimple option is never used (the only caller short-circuits on it, but it seems safer to leave) + // (in contrast, the rest of the condition is used heavily) + return new PolyNumberFormatter(this.intl, opts.forceSimple || this.fastNumbers, opts); + } + + dtFormatter(dt, intlOpts = {}) { + return new PolyDateFormatter(dt, this.intl, intlOpts); + } + + relFormatter(opts = {}) { + return new PolyRelFormatter(this.intl, this.isEnglish(), opts); + } + + listFormatter(opts = {}) { + return getCachedLF(this.intl, opts); + } + + isEnglish() { + return ( + this.locale === "en" || + this.locale.toLowerCase() === "en-us" || + new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith("en-us") + ); + } + + getWeekSettings() { + if (this.weekSettings) { + return this.weekSettings; + } else if (!hasLocaleWeekInfo()) { + return fallbackWeekSettings; + } else { + return getCachedWeekInfo(this.locale); + } + } + + getStartOfWeek() { + return this.getWeekSettings().firstDay; + } + + getMinDaysInFirstWeek() { + return this.getWeekSettings().minimalDays; + } + + getWeekendDays() { + return this.getWeekSettings().weekend; + } + + equals(other) { + return ( + this.locale === other.locale && + this.numberingSystem === other.numberingSystem && + this.outputCalendar === other.outputCalendar + ); + } +} diff --git a/src/shared/libs/luxon/src/impl/regexParser.js b/src/shared/libs/luxon/src/impl/regexParser.js new file mode 100644 index 0000000..ab09586 --- /dev/null +++ b/src/shared/libs/luxon/src/impl/regexParser.js @@ -0,0 +1,335 @@ +import { + untruncateYear, + signedOffset, + parseInteger, + parseMillis, + isUndefined, + parseFloating, +} from "./util.js"; +import * as English from "./english.js"; +import FixedOffsetZone from "../zones/fixedOffsetZone.js"; +import IANAZone from "../zones/IANAZone.js"; + +/* + * This file handles parsing for well-specified formats. Here's how it works: + * Two things go into parsing: a regex to match with and an extractor to take apart the groups in the match. + * An extractor is just a function that takes a regex match array and returns a { year: ..., month: ... } object + * parse() does the work of executing the regex and applying the extractor. It takes multiple regex/extractor pairs to try in sequence. + * Extractors can take a "cursor" representing the offset in the match to look at. This makes it easy to combine extractors. + * combineExtractors() does the work of combining them, keeping track of the cursor through multiple extractions. + * Some extractions are super dumb and simpleParse and fromStrings help DRY them. + */ + +const ianaRegex = /[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/; + +function combineRegexes(...regexes) { + const full = regexes.reduce((f, r) => f + r.source, ""); + return RegExp(`^${full}$`); +} + +function combineExtractors(...extractors) { + return (m) => + extractors + .reduce( + ([mergedVals, mergedZone, cursor], ex) => { + const [val, zone, next] = ex(m, cursor); + return [{ ...mergedVals, ...val }, zone || mergedZone, next]; + }, + [{}, null, 1] + ) + .slice(0, 2); +} + +function parse(s, ...patterns) { + if (s == null) { + return [null, null]; + } + + for (const [regex, extractor] of patterns) { + const m = regex.exec(s); + if (m) { + return extractor(m); + } + } + return [null, null]; +} + +function simpleParse(...keys) { + return (match, cursor) => { + const ret = {}; + let i; + + for (i = 0; i < keys.length; i++) { + ret[keys[i]] = parseInteger(match[cursor + i]); + } + return [ret, null, cursor + i]; + }; +} + +// ISO and SQL parsing +const offsetRegex = /(?:(Z)|([+-]\d\d)(?::?(\d\d))?)/; +const isoExtendedZone = `(?:${offsetRegex.source}?(?:\\[(${ianaRegex.source})\\])?)?`; +const isoTimeBaseRegex = /(\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d{1,30}))?)?)?/; +const isoTimeRegex = RegExp(`${isoTimeBaseRegex.source}${isoExtendedZone}`); +const isoTimeExtensionRegex = RegExp(`(?:T${isoTimeRegex.source})?`); +const isoYmdRegex = /([+-]\d{6}|\d{4})(?:-?(\d\d)(?:-?(\d\d))?)?/; +const isoWeekRegex = /(\d{4})-?W(\d\d)(?:-?(\d))?/; +const isoOrdinalRegex = /(\d{4})-?(\d{3})/; +const extractISOWeekData = simpleParse("weekYear", "weekNumber", "weekDay"); +const extractISOOrdinalData = simpleParse("year", "ordinal"); +const sqlYmdRegex = /(\d{4})-(\d\d)-(\d\d)/; // dumbed-down version of the ISO one +const sqlTimeRegex = RegExp( + `${isoTimeBaseRegex.source} ?(?:${offsetRegex.source}|(${ianaRegex.source}))?` +); +const sqlTimeExtensionRegex = RegExp(`(?: ${sqlTimeRegex.source})?`); + +function int(match, pos, fallback) { + const m = match[pos]; + return isUndefined(m) ? fallback : parseInteger(m); +} + +function extractISOYmd(match, cursor) { + const item = { + year: int(match, cursor), + month: int(match, cursor + 1, 1), + day: int(match, cursor + 2, 1), + }; + + return [item, null, cursor + 3]; +} + +function extractISOTime(match, cursor) { + const item = { + hours: int(match, cursor, 0), + minutes: int(match, cursor + 1, 0), + seconds: int(match, cursor + 2, 0), + milliseconds: parseMillis(match[cursor + 3]), + }; + + return [item, null, cursor + 4]; +} + +function extractISOOffset(match, cursor) { + const local = !match[cursor] && !match[cursor + 1], + fullOffset = signedOffset(match[cursor + 1], match[cursor + 2]), + zone = local ? null : FixedOffsetZone.instance(fullOffset); + return [{}, zone, cursor + 3]; +} + +function extractIANAZone(match, cursor) { + const zone = match[cursor] ? IANAZone.create(match[cursor]) : null; + return [{}, zone, cursor + 1]; +} + +// ISO time parsing + +const isoTimeOnly = RegExp(`^T?${isoTimeBaseRegex.source}$`); + +// ISO duration parsing + +const isoDuration = + /^-?P(?:(?:(-?\d{1,20}(?:\.\d{1,20})?)Y)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20}(?:\.\d{1,20})?)W)?(?:(-?\d{1,20}(?:\.\d{1,20})?)D)?(?:T(?:(-?\d{1,20}(?:\.\d{1,20})?)H)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,20}))?S)?)?)$/; + +function extractISODuration(match) { + const [s, yearStr, monthStr, weekStr, dayStr, hourStr, minuteStr, secondStr, millisecondsStr] = + match; + + const hasNegativePrefix = s[0] === "-"; + const negativeSeconds = secondStr && secondStr[0] === "-"; + + const maybeNegate = (num, force = false) => + num !== undefined && (force || (num && hasNegativePrefix)) ? -num : num; + + return [ + { + years: maybeNegate(parseFloating(yearStr)), + months: maybeNegate(parseFloating(monthStr)), + weeks: maybeNegate(parseFloating(weekStr)), + days: maybeNegate(parseFloating(dayStr)), + hours: maybeNegate(parseFloating(hourStr)), + minutes: maybeNegate(parseFloating(minuteStr)), + seconds: maybeNegate(parseFloating(secondStr), secondStr === "-0"), + milliseconds: maybeNegate(parseMillis(millisecondsStr), negativeSeconds), + }, + ]; +} + +// These are a little braindead. EDT *should* tell us that we're in, say, America/New_York +// and not just that we're in -240 *right now*. But since I don't think these are used that often +// I'm just going to ignore that +const obsOffsets = { + GMT: 0, + EDT: -4 * 60, + EST: -5 * 60, + CDT: -5 * 60, + CST: -6 * 60, + MDT: -6 * 60, + MST: -7 * 60, + PDT: -7 * 60, + PST: -8 * 60, +}; + +function fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr) { + const result = { + year: yearStr.length === 2 ? untruncateYear(parseInteger(yearStr)) : parseInteger(yearStr), + month: English.monthsShort.indexOf(monthStr) + 1, + day: parseInteger(dayStr), + hour: parseInteger(hourStr), + minute: parseInteger(minuteStr), + }; + + if (secondStr) result.second = parseInteger(secondStr); + if (weekdayStr) { + result.weekday = + weekdayStr.length > 3 + ? English.weekdaysLong.indexOf(weekdayStr) + 1 + : English.weekdaysShort.indexOf(weekdayStr) + 1; + } + + return result; +} + +// RFC 2822/5322 +const rfc2822 = + /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\d\d)(\d\d)))$/; + +function extractRFC2822(match) { + const [ + , + weekdayStr, + dayStr, + monthStr, + yearStr, + hourStr, + minuteStr, + secondStr, + obsOffset, + milOffset, + offHourStr, + offMinuteStr, + ] = match, + result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr); + + let offset; + if (obsOffset) { + offset = obsOffsets[obsOffset]; + } else if (milOffset) { + offset = 0; + } else { + offset = signedOffset(offHourStr, offMinuteStr); + } + + return [result, new FixedOffsetZone(offset)]; +} + +function preprocessRFC2822(s) { + // Remove comments and folding whitespace and replace multiple-spaces with a single space + return s + .replace(/\([^()]*\)|[\n\t]/g, " ") + .replace(/(\s\s+)/g, " ") + .trim(); +} + +// http date + +const rfc1123 = + /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/, + rfc850 = + /^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/, + ascii = + /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d) (\d\d):(\d\d):(\d\d) (\d{4})$/; + +function extractRFC1123Or850(match) { + const [, weekdayStr, dayStr, monthStr, yearStr, hourStr, minuteStr, secondStr] = match, + result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr); + return [result, FixedOffsetZone.utcInstance]; +} + +function extractASCII(match) { + const [, weekdayStr, monthStr, dayStr, hourStr, minuteStr, secondStr, yearStr] = match, + result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr); + return [result, FixedOffsetZone.utcInstance]; +} + +const isoYmdWithTimeExtensionRegex = combineRegexes(isoYmdRegex, isoTimeExtensionRegex); +const isoWeekWithTimeExtensionRegex = combineRegexes(isoWeekRegex, isoTimeExtensionRegex); +const isoOrdinalWithTimeExtensionRegex = combineRegexes(isoOrdinalRegex, isoTimeExtensionRegex); +const isoTimeCombinedRegex = combineRegexes(isoTimeRegex); + +const extractISOYmdTimeAndOffset = combineExtractors( + extractISOYmd, + extractISOTime, + extractISOOffset, + extractIANAZone +); +const extractISOWeekTimeAndOffset = combineExtractors( + extractISOWeekData, + extractISOTime, + extractISOOffset, + extractIANAZone +); +const extractISOOrdinalDateAndTime = combineExtractors( + extractISOOrdinalData, + extractISOTime, + extractISOOffset, + extractIANAZone +); +const extractISOTimeAndOffset = combineExtractors( + extractISOTime, + extractISOOffset, + extractIANAZone +); + +/* + * @private + */ + +export function parseISODate(s) { + return parse( + s, + [isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], + [isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset], + [isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime], + [isoTimeCombinedRegex, extractISOTimeAndOffset] + ); +} + +export function parseRFC2822Date(s) { + return parse(preprocessRFC2822(s), [rfc2822, extractRFC2822]); +} + +export function parseHTTPDate(s) { + return parse( + s, + [rfc1123, extractRFC1123Or850], + [rfc850, extractRFC1123Or850], + [ascii, extractASCII] + ); +} + +export function parseISODuration(s) { + return parse(s, [isoDuration, extractISODuration]); +} + +const extractISOTimeOnly = combineExtractors(extractISOTime); + +export function parseISOTimeOnly(s) { + return parse(s, [isoTimeOnly, extractISOTimeOnly]); +} + +const sqlYmdWithTimeExtensionRegex = combineRegexes(sqlYmdRegex, sqlTimeExtensionRegex); +const sqlTimeCombinedRegex = combineRegexes(sqlTimeRegex); + +const extractISOTimeOffsetAndIANAZone = combineExtractors( + extractISOTime, + extractISOOffset, + extractIANAZone +); + +export function parseSQL(s) { + return parse( + s, + [sqlYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], + [sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone] + ); +} diff --git a/src/shared/libs/luxon/src/impl/tokenParser.js b/src/shared/libs/luxon/src/impl/tokenParser.js new file mode 100644 index 0000000..8dd38f3 --- /dev/null +++ b/src/shared/libs/luxon/src/impl/tokenParser.js @@ -0,0 +1,473 @@ +import { parseMillis, isUndefined, untruncateYear, signedOffset, hasOwnProperty } from "./util.js"; +import Formatter from "./formatter.js"; +import FixedOffsetZone from "../zones/fixedOffsetZone.js"; +import IANAZone from "../zones/IANAZone.js"; +import DateTime from "../datetime.js"; +import { digitRegex, parseDigits } from "./digits.js"; +import { ConflictingSpecificationError } from "../errors.js"; + +const MISSING_FTP = "missing Intl.DateTimeFormat.formatToParts support"; + +function intUnit(regex, post = (i) => i) { + return { regex, deser: ([s]) => post(parseDigits(s)) }; +} + +const NBSP = String.fromCharCode(160); +const spaceOrNBSP = `[ ${NBSP}]`; +const spaceOrNBSPRegExp = new RegExp(spaceOrNBSP, "g"); + +function fixListRegex(s) { + // make dots optional and also make them literal + // make space and non breakable space characters interchangeable + return s.replace(/\./g, "\\.?").replace(spaceOrNBSPRegExp, spaceOrNBSP); +} + +function stripInsensitivities(s) { + return s + .replace(/\./g, "") // ignore dots that were made optional + .replace(spaceOrNBSPRegExp, " ") // interchange space and nbsp + .toLowerCase(); +} + +function oneOf(strings, startIndex) { + if (strings === null) { + return null; + } else { + return { + regex: RegExp(strings.map(fixListRegex).join("|")), + deser: ([s]) => + strings.findIndex((i) => stripInsensitivities(s) === stripInsensitivities(i)) + startIndex, + }; + } +} + +function offset(regex, groups) { + return { regex, deser: ([, h, m]) => signedOffset(h, m), groups }; +} + +function simple(regex) { + return { regex, deser: ([s]) => s }; +} + +function escapeToken(value) { + return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); +} + +/** + * @param token + * @param {Locale} loc + */ +function unitForToken(token, loc) { + const one = digitRegex(loc), + two = digitRegex(loc, "{2}"), + three = digitRegex(loc, "{3}"), + four = digitRegex(loc, "{4}"), + six = digitRegex(loc, "{6}"), + oneOrTwo = digitRegex(loc, "{1,2}"), + oneToThree = digitRegex(loc, "{1,3}"), + oneToSix = digitRegex(loc, "{1,6}"), + oneToNine = digitRegex(loc, "{1,9}"), + twoToFour = digitRegex(loc, "{2,4}"), + fourToSix = digitRegex(loc, "{4,6}"), + literal = (t) => ({ regex: RegExp(escapeToken(t.val)), deser: ([s]) => s, literal: true }), + unitate = (t) => { + if (token.literal) { + return literal(t); + } + switch (t.val) { + // era + case "G": + return oneOf(loc.eras("short"), 0); + case "GG": + return oneOf(loc.eras("long"), 0); + // years + case "y": + return intUnit(oneToSix); + case "yy": + return intUnit(twoToFour, untruncateYear); + case "yyyy": + return intUnit(four); + case "yyyyy": + return intUnit(fourToSix); + case "yyyyyy": + return intUnit(six); + // months + case "M": + return intUnit(oneOrTwo); + case "MM": + return intUnit(two); + case "MMM": + return oneOf(loc.months("short", true), 1); + case "MMMM": + return oneOf(loc.months("long", true), 1); + case "L": + return intUnit(oneOrTwo); + case "LL": + return intUnit(two); + case "LLL": + return oneOf(loc.months("short", false), 1); + case "LLLL": + return oneOf(loc.months("long", false), 1); + // dates + case "d": + return intUnit(oneOrTwo); + case "dd": + return intUnit(two); + // ordinals + case "o": + return intUnit(oneToThree); + case "ooo": + return intUnit(three); + // time + case "HH": + return intUnit(two); + case "H": + return intUnit(oneOrTwo); + case "hh": + return intUnit(two); + case "h": + return intUnit(oneOrTwo); + case "mm": + return intUnit(two); + case "m": + return intUnit(oneOrTwo); + case "q": + return intUnit(oneOrTwo); + case "qq": + return intUnit(two); + case "s": + return intUnit(oneOrTwo); + case "ss": + return intUnit(two); + case "S": + return intUnit(oneToThree); + case "SSS": + return intUnit(three); + case "u": + return simple(oneToNine); + case "uu": + return simple(oneOrTwo); + case "uuu": + return intUnit(one); + // meridiem + case "a": + return oneOf(loc.meridiems(), 0); + // weekYear (k) + case "kkkk": + return intUnit(four); + case "kk": + return intUnit(twoToFour, untruncateYear); + // weekNumber (W) + case "W": + return intUnit(oneOrTwo); + case "WW": + return intUnit(two); + // weekdays + case "E": + case "c": + return intUnit(one); + case "EEE": + return oneOf(loc.weekdays("short", false), 1); + case "EEEE": + return oneOf(loc.weekdays("long", false), 1); + case "ccc": + return oneOf(loc.weekdays("short", true), 1); + case "cccc": + return oneOf(loc.weekdays("long", true), 1); + // offset/zone + case "Z": + case "ZZ": + return offset(new RegExp(`([+-]${oneOrTwo.source})(?::(${two.source}))?`), 2); + case "ZZZ": + return offset(new RegExp(`([+-]${oneOrTwo.source})(${two.source})?`), 2); + // we don't support ZZZZ (PST) or ZZZZZ (Pacific Standard Time) in parsing + // because we don't have any way to figure out what they are + case "z": + return simple(/[a-z_+-/]{1,256}?/i); + // this special-case "token" represents a place where a macro-token expanded into a white-space literal + // in this case we accept any non-newline white-space + case " ": + return simple(/[^\S\n\r]/); + default: + return literal(t); + } + }; + + const unit = unitate(token) || { + invalidReason: MISSING_FTP, + }; + + unit.token = token; + + return unit; +} + +const partTypeStyleToTokenVal = { + year: { + "2-digit": "yy", + numeric: "yyyyy", + }, + month: { + numeric: "M", + "2-digit": "MM", + short: "MMM", + long: "MMMM", + }, + day: { + numeric: "d", + "2-digit": "dd", + }, + weekday: { + short: "EEE", + long: "EEEE", + }, + dayperiod: "a", + dayPeriod: "a", + hour12: { + numeric: "h", + "2-digit": "hh", + }, + hour24: { + numeric: "H", + "2-digit": "HH", + }, + minute: { + numeric: "m", + "2-digit": "mm", + }, + second: { + numeric: "s", + "2-digit": "ss", + }, + timeZoneName: { + long: "ZZZZZ", + short: "ZZZ", + }, +}; + +function tokenForPart(part, formatOpts, resolvedOpts) { + const { type, value } = part; + + if (type === "literal") { + const isSpace = /^\s+$/.test(value); + return { + literal: !isSpace, + val: isSpace ? " " : value, + }; + } + + const style = formatOpts[type]; + + // The user might have explicitly specified hour12 or hourCycle + // if so, respect their decision + // if not, refer back to the resolvedOpts, which are based on the locale + let actualType = type; + if (type === "hour") { + if (formatOpts.hour12 != null) { + actualType = formatOpts.hour12 ? "hour12" : "hour24"; + } else if (formatOpts.hourCycle != null) { + if (formatOpts.hourCycle === "h11" || formatOpts.hourCycle === "h12") { + actualType = "hour12"; + } else { + actualType = "hour24"; + } + } else { + // tokens only differentiate between 24 hours or not, + // so we do not need to check hourCycle here, which is less supported anyways + actualType = resolvedOpts.hour12 ? "hour12" : "hour24"; + } + } + let val = partTypeStyleToTokenVal[actualType]; + if (typeof val === "object") { + val = val[style]; + } + + if (val) { + return { + literal: false, + val, + }; + } + + return undefined; +} + +function buildRegex(units) { + const re = units.map((u) => u.regex).reduce((f, r) => `${f}(${r.source})`, ""); + return [`^${re}$`, units]; +} + +function match(input, regex, handlers) { + const matches = input.match(regex); + + if (matches) { + const all = {}; + let matchIndex = 1; + for (const i in handlers) { + if (hasOwnProperty(handlers, i)) { + const h = handlers[i], + groups = h.groups ? h.groups + 1 : 1; + if (!h.literal && h.token) { + all[h.token.val[0]] = h.deser(matches.slice(matchIndex, matchIndex + groups)); + } + matchIndex += groups; + } + } + return [matches, all]; + } else { + return [matches, {}]; + } +} + +function dateTimeFromMatches(matches) { + const toField = (token) => { + switch (token) { + case "S": + return "millisecond"; + case "s": + return "second"; + case "m": + return "minute"; + case "h": + case "H": + return "hour"; + case "d": + return "day"; + case "o": + return "ordinal"; + case "L": + case "M": + return "month"; + case "y": + return "year"; + case "E": + case "c": + return "weekday"; + case "W": + return "weekNumber"; + case "k": + return "weekYear"; + case "q": + return "quarter"; + default: + return null; + } + }; + + let zone = null; + let specificOffset; + if (!isUndefined(matches.z)) { + zone = IANAZone.create(matches.z); + } + + if (!isUndefined(matches.Z)) { + if (!zone) { + zone = new FixedOffsetZone(matches.Z); + } + specificOffset = matches.Z; + } + + if (!isUndefined(matches.q)) { + matches.M = (matches.q - 1) * 3 + 1; + } + + if (!isUndefined(matches.h)) { + if (matches.h < 12 && matches.a === 1) { + matches.h += 12; + } else if (matches.h === 12 && matches.a === 0) { + matches.h = 0; + } + } + + if (matches.G === 0 && matches.y) { + matches.y = -matches.y; + } + + if (!isUndefined(matches.u)) { + matches.S = parseMillis(matches.u); + } + + const vals = Object.keys(matches).reduce((r, k) => { + const f = toField(k); + if (f) { + r[f] = matches[k]; + } + + return r; + }, {}); + + return [vals, zone, specificOffset]; +} + +let dummyDateTimeCache = null; + +function getDummyDateTime() { + if (!dummyDateTimeCache) { + dummyDateTimeCache = DateTime.fromMillis(1555555555555); + } + + return dummyDateTimeCache; +} + +function maybeExpandMacroToken(token, locale) { + if (token.literal) { + return token; + } + + const formatOpts = Formatter.macroTokenToFormatOpts(token.val); + const tokens = formatOptsToTokens(formatOpts, locale); + + if (tokens == null || tokens.includes(undefined)) { + return token; + } + + return tokens; +} + +export function expandMacroTokens(tokens, locale) { + return Array.prototype.concat(...tokens.map((t) => maybeExpandMacroToken(t, locale))); +} + +/** + * @private + */ + +export function explainFromTokens(locale, input, format) { + const tokens = expandMacroTokens(Formatter.parseFormat(format), locale), + units = tokens.map((t) => unitForToken(t, locale)), + disqualifyingUnit = units.find((t) => t.invalidReason); + + if (disqualifyingUnit) { + return { input, tokens, invalidReason: disqualifyingUnit.invalidReason }; + } else { + const [regexString, handlers] = buildRegex(units), + regex = RegExp(regexString, "i"), + [rawMatches, matches] = match(input, regex, handlers), + [result, zone, specificOffset] = matches + ? dateTimeFromMatches(matches) + : [null, null, undefined]; + if (hasOwnProperty(matches, "a") && hasOwnProperty(matches, "H")) { + throw new ConflictingSpecificationError( + "Can't include meridiem when specifying 24-hour format" + ); + } + return { input, tokens, regex, rawMatches, matches, result, zone, specificOffset }; + } +} + +export function parseFromTokens(locale, input, format) { + const { result, zone, specificOffset, invalidReason } = explainFromTokens(locale, input, format); + return [result, zone, specificOffset, invalidReason]; +} + +export function formatOptsToTokens(formatOpts, locale) { + if (!formatOpts) { + return null; + } + + const formatter = Formatter.create(locale, formatOpts); + const df = formatter.dtFormatter(getDummyDateTime()); + const parts = df.formatToParts(); + const resolvedOpts = df.resolvedOptions(); + return parts.map((p) => tokenForPart(p, formatOpts, resolvedOpts)); +} diff --git a/src/shared/libs/luxon/src/impl/util.js b/src/shared/libs/luxon/src/impl/util.js new file mode 100644 index 0000000..4051924 --- /dev/null +++ b/src/shared/libs/luxon/src/impl/util.js @@ -0,0 +1,309 @@ +/* + This is just a junk drawer, containing anything used across multiple classes. + Because Luxon is small(ish), this should stay small and we won't worry about splitting + it up into, say, parsingUtil.js and basicUtil.js and so on. But they are divided up by feature area. +*/ + +import { InvalidArgumentError } from "../errors.js"; +import Settings from "../settings.js"; +import { dayOfWeek, isoWeekdayToLocal } from "./conversions.js"; + +/** + * @private + */ + +// TYPES + +export function isUndefined(o) { + return typeof o === "undefined"; +} + +export function isNumber(o) { + return typeof o === "number"; +} + +export function isInteger(o) { + return typeof o === "number" && o % 1 === 0; +} + +export function isString(o) { + return typeof o === "string"; +} + +export function isDate(o) { + return Object.prototype.toString.call(o) === "[object Date]"; +} + +// CAPABILITIES + +export function hasRelative() { + try { + return typeof Intl !== "undefined" && !!Intl.RelativeTimeFormat; + } catch (e) { + return false; + } +} + +export function hasLocaleWeekInfo() { + try { + return ( + typeof Intl !== "undefined" && + !!Intl.Locale && + ("weekInfo" in Intl.Locale.prototype || "getWeekInfo" in Intl.Locale.prototype) + ); + } catch (e) { + return false; + } +} + +// OBJECTS AND ARRAYS + +export function maybeArray(thing) { + return Array.isArray(thing) ? thing : [thing]; +} + +export function bestBy(arr, by, compare) { + if (arr.length === 0) { + return undefined; + } + return arr.reduce((best, next) => { + const pair = [by(next), next]; + if (!best) { + return pair; + } else if (compare(best[0], pair[0]) === best[0]) { + return best; + } else { + return pair; + } + }, null)[1]; +} + +export function pick(obj, keys) { + return keys.reduce((a, k) => { + a[k] = obj[k]; + return a; + }, {}); +} + +export function hasOwnProperty(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +export function validateWeekSettings(settings) { + if (settings == null) { + return null; + } else if (typeof settings !== "object") { + throw new InvalidArgumentError("Week settings must be an object"); + } else { + if ( + !integerBetween(settings.firstDay, 1, 7) || + !integerBetween(settings.minimalDays, 1, 7) || + !Array.isArray(settings.weekend) || + settings.weekend.some((v) => !integerBetween(v, 1, 7)) + ) { + throw new InvalidArgumentError("Invalid week settings"); + } + return { + firstDay: settings.firstDay, + minimalDays: settings.minimalDays, + weekend: Array.from(settings.weekend), + }; + } +} + +// NUMBERS AND STRINGS + +export function integerBetween(thing, bottom, top) { + return isInteger(thing) && thing >= bottom && thing <= top; +} + +// x % n but takes the sign of n instead of x +export function floorMod(x, n) { + return x - n * Math.floor(x / n); +} + +export function padStart(input, n = 2) { + const isNeg = input < 0; + let padded; + if (isNeg) { + padded = "-" + ("" + -input).padStart(n, "0"); + } else { + padded = ("" + input).padStart(n, "0"); + } + return padded; +} + +export function parseInteger(string) { + if (isUndefined(string) || string === null || string === "") { + return undefined; + } else { + return parseInt(string, 10); + } +} + +export function parseFloating(string) { + if (isUndefined(string) || string === null || string === "") { + return undefined; + } else { + return parseFloat(string); + } +} + +export function parseMillis(fraction) { + // Return undefined (instead of 0) in these cases, where fraction is not set + if (isUndefined(fraction) || fraction === null || fraction === "") { + return undefined; + } else { + const f = parseFloat("0." + fraction) * 1000; + return Math.floor(f); + } +} + +export function roundTo(number, digits, towardZero = false) { + const factor = 10 ** digits, + rounder = towardZero ? Math.trunc : Math.round; + return rounder(number * factor) / factor; +} + +// DATE BASICS + +export function isLeapYear(year) { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} + +export function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; +} + +export function daysInMonth(year, month) { + const modMonth = floorMod(month - 1, 12) + 1, + modYear = year + (month - modMonth) / 12; + + if (modMonth === 2) { + return isLeapYear(modYear) ? 29 : 28; + } else { + return [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][modMonth - 1]; + } +} + +// convert a calendar object to a local timestamp (epoch, but with the offset baked in) +export function objToLocalTS(obj) { + let d = Date.UTC( + obj.year, + obj.month - 1, + obj.day, + obj.hour, + obj.minute, + obj.second, + obj.millisecond + ); + + // for legacy reasons, years between 0 and 99 are interpreted as 19XX; revert that + if (obj.year < 100 && obj.year >= 0) { + d = new Date(d); + // set the month and day again, this is necessary because year 2000 is a leap year, but year 100 is not + // so if obj.year is in 99, but obj.day makes it roll over into year 100, + // the calculations done by Date.UTC are using year 2000 - which is incorrect + d.setUTCFullYear(obj.year, obj.month - 1, obj.day); + } + return +d; +} + +// adapted from moment.js: https://github.com/moment/moment/blob/000ac1800e620f770f4eb31b5ae908f6167b0ab2/src/lib/units/week-calendar-utils.js +function firstWeekOffset(year, minDaysInFirstWeek, startOfWeek) { + const fwdlw = isoWeekdayToLocal(dayOfWeek(year, 1, minDaysInFirstWeek), startOfWeek); + return -fwdlw + minDaysInFirstWeek - 1; +} + +export function weeksInWeekYear(weekYear, minDaysInFirstWeek = 4, startOfWeek = 1) { + const weekOffset = firstWeekOffset(weekYear, minDaysInFirstWeek, startOfWeek); + const weekOffsetNext = firstWeekOffset(weekYear + 1, minDaysInFirstWeek, startOfWeek); + return (daysInYear(weekYear) - weekOffset + weekOffsetNext) / 7; +} + +export function untruncateYear(year) { + if (year > 99) { + return year; + } else return year > Settings.twoDigitCutoffYear ? 1900 + year : 2000 + year; +} + +// PARSING + +export function parseZoneInfo(ts, offsetFormat, locale, timeZone = null) { + const date = new Date(ts), + intlOpts = { + hourCycle: "h23", + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }; + + if (timeZone) { + intlOpts.timeZone = timeZone; + } + + const modified = { timeZoneName: offsetFormat, ...intlOpts }; + + const parsed = new Intl.DateTimeFormat(locale, modified) + .formatToParts(date) + .find((m) => m.type.toLowerCase() === "timezonename"); + return parsed ? parsed.value : null; +} + +// signedOffset('-5', '30') -> -330 +export function signedOffset(offHourStr, offMinuteStr) { + let offHour = parseInt(offHourStr, 10); + + // don't || this because we want to preserve -0 + if (Number.isNaN(offHour)) { + offHour = 0; + } + + const offMin = parseInt(offMinuteStr, 10) || 0, + offMinSigned = offHour < 0 || Object.is(offHour, -0) ? -offMin : offMin; + return offHour * 60 + offMinSigned; +} + +// COERCION + +export function asNumber(value) { + const numericValue = Number(value); + if (typeof value === "boolean" || value === "" || Number.isNaN(numericValue)) + throw new InvalidArgumentError(`Invalid unit value ${value}`); + return numericValue; +} + +export function normalizeObject(obj, normalizer) { + const normalized = {}; + for (const u in obj) { + if (hasOwnProperty(obj, u)) { + const v = obj[u]; + if (v === undefined || v === null) continue; + normalized[normalizer(u)] = asNumber(v); + } + } + return normalized; +} + +export function formatOffset(offset, format) { + const hours = Math.trunc(Math.abs(offset / 60)), + minutes = Math.trunc(Math.abs(offset % 60)), + sign = offset >= 0 ? "+" : "-"; + + switch (format) { + case "short": + return `${sign}${padStart(hours, 2)}:${padStart(minutes, 2)}`; + case "narrow": + return `${sign}${hours}${minutes > 0 ? `:${minutes}` : ""}`; + case "techie": + return `${sign}${padStart(hours, 2)}${padStart(minutes, 2)}`; + default: + throw new RangeError(`Value format ${format} is out of range for property format`); + } +} + +export function timeObject(obj) { + return pick(obj, ["hour", "minute", "second", "millisecond"]); +} diff --git a/src/shared/libs/luxon/src/impl/zoneUtil.js b/src/shared/libs/luxon/src/impl/zoneUtil.js new file mode 100644 index 0000000..c3151a7 --- /dev/null +++ b/src/shared/libs/luxon/src/impl/zoneUtil.js @@ -0,0 +1,34 @@ +/** + * @private + */ + +import Zone from "../zone.js"; +import IANAZone from "../zones/IANAZone.js"; +import FixedOffsetZone from "../zones/fixedOffsetZone.js"; +import InvalidZone from "../zones/invalidZone.js"; + +import { isUndefined, isString, isNumber } from "./util.js"; +import SystemZone from "../zones/systemZone.js"; + +export function normalizeZone(input, defaultZone) { + let offset; + if (isUndefined(input) || input === null) { + return defaultZone; + } else if (input instanceof Zone) { + return input; + } else if (isString(input)) { + const lowered = input.toLowerCase(); + if (lowered === "default") return defaultZone; + else if (lowered === "local" || lowered === "system") return SystemZone.instance; + else if (lowered === "utc" || lowered === "gmt") return FixedOffsetZone.utcInstance; + else return FixedOffsetZone.parseSpecifier(lowered) || IANAZone.create(input); + } else if (isNumber(input)) { + return FixedOffsetZone.instance(input); + } else if (typeof input === "object" && "offset" in input && typeof input.offset === "function") { + // This is dumb, but the instanceof check above doesn't seem to really work + // so we're duck checking it + return input; + } else { + return new InvalidZone(input); + } +} diff --git a/src/shared/libs/luxon/src/info.js b/src/shared/libs/luxon/src/info.js new file mode 100644 index 0000000..72124b4 --- /dev/null +++ b/src/shared/libs/luxon/src/info.js @@ -0,0 +1,205 @@ +import DateTime from "./datetime.js"; +import Settings from "./settings.js"; +import Locale from "./impl/locale.js"; +import IANAZone from "./zones/IANAZone.js"; +import { normalizeZone } from "./impl/zoneUtil.js"; + +import { hasLocaleWeekInfo, hasRelative } from "./impl/util.js"; + +/** + * The Info class contains static methods for retrieving general time and date related data. For example, it has methods for finding out if a time zone has a DST, for listing the months in any supported locale, and for discovering which of Luxon features are available in the current environment. + */ +export default class Info { + /** + * Return whether the specified zone contains a DST. + * @param {string|Zone} [zone='local'] - Zone to check. Defaults to the environment's local zone. + * @return {boolean} + */ + static hasDST(zone = Settings.defaultZone) { + const proto = DateTime.now().setZone(zone).set({ month: 12 }); + + return !zone.isUniversal && proto.offset !== proto.set({ month: 6 }).offset; + } + + /** + * Return whether the specified zone is a valid IANA specifier. + * @param {string} zone - Zone to check + * @return {boolean} + */ + static isValidIANAZone(zone) { + return IANAZone.isValidZone(zone); + } + + /** + * Converts the input into a {@link Zone} instance. + * + * * If `input` is already a Zone instance, it is returned unchanged. + * * If `input` is a string containing a valid time zone name, a Zone instance + * with that name is returned. + * * If `input` is a string that doesn't refer to a known time zone, a Zone + * instance with {@link Zone#isValid} == false is returned. + * * If `input is a number, a Zone instance with the specified fixed offset + * in minutes is returned. + * * If `input` is `null` or `undefined`, the default zone is returned. + * @param {string|Zone|number} [input] - the value to be converted + * @return {Zone} + */ + static normalizeZone(input) { + return normalizeZone(input, Settings.defaultZone); + } + + /** + * Get the weekday on which the week starts according to the given locale. + * @param {Object} opts - options + * @param {string} [opts.locale] - the locale code + * @param {string} [opts.locObj=null] - an existing locale object to use + * @returns {number} the start of the week, 1 for Monday through 7 for Sunday + */ + static getStartOfWeek({ locale = null, locObj = null } = {}) { + return (locObj || Locale.create(locale)).getStartOfWeek(); + } + + /** + * Get the minimum number of days necessary in a week before it is considered part of the next year according + * to the given locale. + * @param {Object} opts - options + * @param {string} [opts.locale] - the locale code + * @param {string} [opts.locObj=null] - an existing locale object to use + * @returns {number} + */ + static getMinimumDaysInFirstWeek({ locale = null, locObj = null } = {}) { + return (locObj || Locale.create(locale)).getMinDaysInFirstWeek(); + } + + /** + * Get the weekdays, which are considered the weekend according to the given locale + * @param {Object} opts - options + * @param {string} [opts.locale] - the locale code + * @param {string} [opts.locObj=null] - an existing locale object to use + * @returns {number[]} an array of weekdays, 1 for Monday through 7 for Sunday + */ + static getWeekendWeekdays({ locale = null, locObj = null } = {}) { + // copy the array, because we cache it internally + return (locObj || Locale.create(locale)).getWeekendDays().slice(); + } + + /** + * Return an array of standalone month names. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat + * @param {string} [length='long'] - the length of the month representation, such as "numeric", "2-digit", "narrow", "short", "long" + * @param {Object} opts - options + * @param {string} [opts.locale] - the locale code + * @param {string} [opts.numberingSystem=null] - the numbering system + * @param {string} [opts.locObj=null] - an existing locale object to use + * @param {string} [opts.outputCalendar='gregory'] - the calendar + * @example Info.months()[0] //=> 'January' + * @example Info.months('short')[0] //=> 'Jan' + * @example Info.months('numeric')[0] //=> '1' + * @example Info.months('short', { locale: 'fr-CA' } )[0] //=> 'janv.' + * @example Info.months('numeric', { locale: 'ar' })[0] //=> '١' + * @example Info.months('long', { outputCalendar: 'islamic' })[0] //=> 'Rabiʻ I' + * @return {Array} + */ + static months( + length = "long", + { locale = null, numberingSystem = null, locObj = null, outputCalendar = "gregory" } = {} + ) { + return (locObj || Locale.create(locale, numberingSystem, outputCalendar)).months(length); + } + + /** + * Return an array of format month names. + * Format months differ from standalone months in that they're meant to appear next to the day of the month. In some languages, that + * changes the string. + * See {@link Info#months} + * @param {string} [length='long'] - the length of the month representation, such as "numeric", "2-digit", "narrow", "short", "long" + * @param {Object} opts - options + * @param {string} [opts.locale] - the locale code + * @param {string} [opts.numberingSystem=null] - the numbering system + * @param {string} [opts.locObj=null] - an existing locale object to use + * @param {string} [opts.outputCalendar='gregory'] - the calendar + * @return {Array} + */ + static monthsFormat( + length = "long", + { locale = null, numberingSystem = null, locObj = null, outputCalendar = "gregory" } = {} + ) { + return (locObj || Locale.create(locale, numberingSystem, outputCalendar)).months(length, true); + } + + /** + * Return an array of standalone week names. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat + * @param {string} [length='long'] - the length of the weekday representation, such as "narrow", "short", "long". + * @param {Object} opts - options + * @param {string} [opts.locale] - the locale code + * @param {string} [opts.numberingSystem=null] - the numbering system + * @param {string} [opts.locObj=null] - an existing locale object to use + * @example Info.weekdays()[0] //=> 'Monday' + * @example Info.weekdays('short')[0] //=> 'Mon' + * @example Info.weekdays('short', { locale: 'fr-CA' })[0] //=> 'lun.' + * @example Info.weekdays('short', { locale: 'ar' })[0] //=> 'الاثنين' + * @return {Array} + */ + static weekdays(length = "long", { locale = null, numberingSystem = null, locObj = null } = {}) { + return (locObj || Locale.create(locale, numberingSystem, null)).weekdays(length); + } + + /** + * Return an array of format week names. + * Format weekdays differ from standalone weekdays in that they're meant to appear next to more date information. In some languages, that + * changes the string. + * See {@link Info#weekdays} + * @param {string} [length='long'] - the length of the month representation, such as "narrow", "short", "long". + * @param {Object} opts - options + * @param {string} [opts.locale=null] - the locale code + * @param {string} [opts.numberingSystem=null] - the numbering system + * @param {string} [opts.locObj=null] - an existing locale object to use + * @return {Array} + */ + static weekdaysFormat( + length = "long", + { locale = null, numberingSystem = null, locObj = null } = {} + ) { + return (locObj || Locale.create(locale, numberingSystem, null)).weekdays(length, true); + } + + /** + * Return an array of meridiems. + * @param {Object} opts - options + * @param {string} [opts.locale] - the locale code + * @example Info.meridiems() //=> [ 'AM', 'PM' ] + * @example Info.meridiems({ locale: 'my' }) //=> [ 'နံနက်', 'ညနေ' ] + * @return {Array} + */ + static meridiems({ locale = null } = {}) { + return Locale.create(locale).meridiems(); + } + + /** + * Return an array of eras, such as ['BC', 'AD']. The locale can be specified, but the calendar system is always Gregorian. + * @param {string} [length='short'] - the length of the era representation, such as "short" or "long". + * @param {Object} opts - options + * @param {string} [opts.locale] - the locale code + * @example Info.eras() //=> [ 'BC', 'AD' ] + * @example Info.eras('long') //=> [ 'Before Christ', 'Anno Domini' ] + * @example Info.eras('long', { locale: 'fr' }) //=> [ 'avant Jésus-Christ', 'après Jésus-Christ' ] + * @return {Array} + */ + static eras(length = "short", { locale = null } = {}) { + return Locale.create(locale, null, "gregory").eras(length); + } + + /** + * Return the set of available features in this environment. + * Some features of Luxon are not available in all environments. For example, on older browsers, relative time formatting support is not available. Use this function to figure out if that's the case. + * Keys: + * * `relative`: whether this environment supports relative time formatting + * * `localeWeek`: whether this environment supports different weekdays for the start of the week based on the locale + * @example Info.features() //=> { relative: false, localeWeek: true } + * @return {Object} + */ + static features() { + return { relative: hasRelative(), localeWeek: hasLocaleWeekInfo() }; + } +} diff --git a/src/shared/libs/luxon/src/interval.js b/src/shared/libs/luxon/src/interval.js new file mode 100644 index 0000000..dfb58d0 --- /dev/null +++ b/src/shared/libs/luxon/src/interval.js @@ -0,0 +1,657 @@ +import DateTime, { friendlyDateTime } from "./datetime.js"; +import Duration from "./duration.js"; +import Settings from "./settings.js"; +import { InvalidArgumentError, InvalidIntervalError } from "./errors.js"; +import Invalid from "./impl/invalid.js"; +import Formatter from "./impl/formatter.js"; +import * as Formats from "./impl/formats.js"; + +const INVALID = "Invalid Interval"; + +// checks if the start is equal to or before the end +function validateStartEnd(start, end) { + if (!start || !start.isValid) { + return Interval.invalid("missing or invalid start"); + } else if (!end || !end.isValid) { + return Interval.invalid("missing or invalid end"); + } else if (end < start) { + return Interval.invalid( + "end before start", + `The end of an interval must be after its start, but you had start=${start.toISO()} and end=${end.toISO()}` + ); + } else { + return null; + } +} + +/** + * An Interval object represents a half-open interval of time, where each endpoint is a {@link DateTime}. Conceptually, it's a container for those two endpoints, accompanied by methods for creating, parsing, interrogating, comparing, transforming, and formatting them. + * + * Here is a brief overview of the most commonly used methods and getters in Interval: + * + * * **Creation** To create an Interval, use {@link Interval.fromDateTimes}, {@link Interval.after}, {@link Interval.before}, or {@link Interval.fromISO}. + * * **Accessors** Use {@link Interval#start} and {@link Interval#end} to get the start and end. + * * **Interrogation** To analyze the Interval, use {@link Interval#count}, {@link Interval#length}, {@link Interval#hasSame}, {@link Interval#contains}, {@link Interval#isAfter}, or {@link Interval#isBefore}. + * * **Transformation** To create other Intervals out of this one, use {@link Interval#set}, {@link Interval#splitAt}, {@link Interval#splitBy}, {@link Interval#divideEqually}, {@link Interval.merge}, {@link Interval.xor}, {@link Interval#union}, {@link Interval#intersection}, or {@link Interval#difference}. + * * **Comparison** To compare this Interval to another one, use {@link Interval#equals}, {@link Interval#overlaps}, {@link Interval#abutsStart}, {@link Interval#abutsEnd}, {@link Interval#engulfs} + * * **Output** To convert the Interval into other representations, see {@link Interval#toString}, {@link Interval#toLocaleString}, {@link Interval#toISO}, {@link Interval#toISODate}, {@link Interval#toISOTime}, {@link Interval#toFormat}, and {@link Interval#toDuration}. + */ +export default class Interval { + /** + * @private + */ + constructor(config) { + /** + * @access private + */ + this.s = config.start; + /** + * @access private + */ + this.e = config.end; + /** + * @access private + */ + this.invalid = config.invalid || null; + /** + * @access private + */ + this.isLuxonInterval = true; + } + + /** + * Create an invalid Interval. + * @param {string} reason - simple string of why this Interval is invalid. Should not contain parameters or anything else data-dependent + * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information + * @return {Interval} + */ + static invalid(reason, explanation = null) { + if (!reason) { + throw new InvalidArgumentError("need to specify a reason the Interval is invalid"); + } + + const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation); + + if (Settings.throwOnInvalid) { + throw new InvalidIntervalError(invalid); + } else { + return new Interval({ invalid }); + } + } + + /** + * Create an Interval from a start DateTime and an end DateTime. Inclusive of the start but not the end. + * @param {DateTime|Date|Object} start + * @param {DateTime|Date|Object} end + * @return {Interval} + */ + static fromDateTimes(start, end) { + const builtStart = friendlyDateTime(start), + builtEnd = friendlyDateTime(end); + + const validateError = validateStartEnd(builtStart, builtEnd); + + if (validateError == null) { + return new Interval({ + start: builtStart, + end: builtEnd, + }); + } else { + return validateError; + } + } + + /** + * Create an Interval from a start DateTime and a Duration to extend to. + * @param {DateTime|Date|Object} start + * @param {Duration|Object|number} duration - the length of the Interval. + * @return {Interval} + */ + static after(start, duration) { + const dur = Duration.fromDurationLike(duration), + dt = friendlyDateTime(start); + return Interval.fromDateTimes(dt, dt.plus(dur)); + } + + /** + * Create an Interval from an end DateTime and a Duration to extend backwards to. + * @param {DateTime|Date|Object} end + * @param {Duration|Object|number} duration - the length of the Interval. + * @return {Interval} + */ + static before(end, duration) { + const dur = Duration.fromDurationLike(duration), + dt = friendlyDateTime(end); + return Interval.fromDateTimes(dt.minus(dur), dt); + } + + /** + * Create an Interval from an ISO 8601 string. + * Accepts `<start>/<end>`, `<start>/<duration>`, and `<duration>/<end>` formats. + * @param {string} text - the ISO string to parse + * @param {Object} [opts] - options to pass {@link DateTime#fromISO} and optionally {@link Duration#fromISO} + * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals + * @return {Interval} + */ + static fromISO(text, opts) { + const [s, e] = (text || "").split("/", 2); + if (s && e) { + let start, startIsValid; + try { + start = DateTime.fromISO(s, opts); + startIsValid = start.isValid; + } catch (e) { + startIsValid = false; + } + + let end, endIsValid; + try { + end = DateTime.fromISO(e, opts); + endIsValid = end.isValid; + } catch (e) { + endIsValid = false; + } + + if (startIsValid && endIsValid) { + return Interval.fromDateTimes(start, end); + } + + if (startIsValid) { + const dur = Duration.fromISO(e, opts); + if (dur.isValid) { + return Interval.after(start, dur); + } + } else if (endIsValid) { + const dur = Duration.fromISO(s, opts); + if (dur.isValid) { + return Interval.before(end, dur); + } + } + } + return Interval.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`); + } + + /** + * Check if an object is an Interval. Works across context boundaries + * @param {object} o + * @return {boolean} + */ + static isInterval(o) { + return (o && o.isLuxonInterval) || false; + } + + /** + * Returns the start of the Interval + * @type {DateTime} + */ + get start() { + return this.isValid ? this.s : null; + } + + /** + * Returns the end of the Interval + * @type {DateTime} + */ + get end() { + return this.isValid ? this.e : null; + } + + /** + * Returns whether this Interval's end is at least its start, meaning that the Interval isn't 'backwards'. + * @type {boolean} + */ + get isValid() { + return this.invalidReason === null; + } + + /** + * Returns an error code if this Interval is invalid, or null if the Interval is valid + * @type {string} + */ + get invalidReason() { + return this.invalid ? this.invalid.reason : null; + } + + /** + * Returns an explanation of why this Interval became invalid, or null if the Interval is valid + * @type {string} + */ + get invalidExplanation() { + return this.invalid ? this.invalid.explanation : null; + } + + /** + * Returns the length of the Interval in the specified unit. + * @param {string} unit - the unit (such as 'hours' or 'days') to return the length in. + * @return {number} + */ + length(unit = "milliseconds") { + return this.isValid ? this.toDuration(...[unit]).get(unit) : NaN; + } + + /** + * Returns the count of minutes, hours, days, months, or years included in the Interval, even in part. + * Unlike {@link Interval#length} this counts sections of the calendar, not periods of time, e.g. specifying 'day' + * asks 'what dates are included in this interval?', not 'how many days long is this interval?' + * @param {string} [unit='milliseconds'] - the unit of time to count. + * @param {Object} opts - options + * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week; this operation will always use the locale of the start DateTime + * @return {number} + */ + count(unit = "milliseconds", opts) { + if (!this.isValid) return NaN; + const start = this.start.startOf(unit, opts); + let end; + if (opts?.useLocaleWeeks) { + end = this.end.reconfigure({ locale: start.locale }); + } else { + end = this.end; + } + end = end.startOf(unit, opts); + return Math.floor(end.diff(start, unit).get(unit)) + (end.valueOf() !== this.end.valueOf()); + } + + /** + * Returns whether this Interval's start and end are both in the same unit of time + * @param {string} unit - the unit of time to check sameness on + * @return {boolean} + */ + hasSame(unit) { + return this.isValid ? this.isEmpty() || this.e.minus(1).hasSame(this.s, unit) : false; + } + + /** + * Return whether this Interval has the same start and end DateTimes. + * @return {boolean} + */ + isEmpty() { + return this.s.valueOf() === this.e.valueOf(); + } + + /** + * Return whether this Interval's start is after the specified DateTime. + * @param {DateTime} dateTime + * @return {boolean} + */ + isAfter(dateTime) { + if (!this.isValid) return false; + return this.s > dateTime; + } + + /** + * Return whether this Interval's end is before the specified DateTime. + * @param {DateTime} dateTime + * @return {boolean} + */ + isBefore(dateTime) { + if (!this.isValid) return false; + return this.e <= dateTime; + } + + /** + * Return whether this Interval contains the specified DateTime. + * @param {DateTime} dateTime + * @return {boolean} + */ + contains(dateTime) { + if (!this.isValid) return false; + return this.s <= dateTime && this.e > dateTime; + } + + /** + * "Sets" the start and/or end dates. Returns a newly-constructed Interval. + * @param {Object} values - the values to set + * @param {DateTime} values.start - the starting DateTime + * @param {DateTime} values.end - the ending DateTime + * @return {Interval} + */ + set({ start, end } = {}) { + if (!this.isValid) return this; + return Interval.fromDateTimes(start || this.s, end || this.e); + } + + /** + * Split this Interval at each of the specified DateTimes + * @param {...DateTime} dateTimes - the unit of time to count. + * @return {Array} + */ + splitAt(...dateTimes) { + if (!this.isValid) return []; + const sorted = dateTimes + .map(friendlyDateTime) + .filter((d) => this.contains(d)) + .sort((a, b) => a.toMillis() - b.toMillis()), + results = []; + let { s } = this, + i = 0; + + while (s < this.e) { + const added = sorted[i] || this.e, + next = +added > +this.e ? this.e : added; + results.push(Interval.fromDateTimes(s, next)); + s = next; + i += 1; + } + + return results; + } + + /** + * Split this Interval into smaller Intervals, each of the specified length. + * Left over time is grouped into a smaller interval + * @param {Duration|Object|number} duration - The length of each resulting interval. + * @return {Array} + */ + splitBy(duration) { + const dur = Duration.fromDurationLike(duration); + + if (!this.isValid || !dur.isValid || dur.as("milliseconds") === 0) { + return []; + } + + let { s } = this, + idx = 1, + next; + + const results = []; + while (s < this.e) { + const added = this.start.plus(dur.mapUnits((x) => x * idx)); + next = +added > +this.e ? this.e : added; + results.push(Interval.fromDateTimes(s, next)); + s = next; + idx += 1; + } + + return results; + } + + /** + * Split this Interval into the specified number of smaller intervals. + * @param {number} numberOfParts - The number of Intervals to divide the Interval into. + * @return {Array} + */ + divideEqually(numberOfParts) { + if (!this.isValid) return []; + return this.splitBy(this.length() / numberOfParts).slice(0, numberOfParts); + } + + /** + * Return whether this Interval overlaps with the specified Interval + * @param {Interval} other + * @return {boolean} + */ + overlaps(other) { + return this.e > other.s && this.s < other.e; + } + + /** + * Return whether this Interval's end is adjacent to the specified Interval's start. + * @param {Interval} other + * @return {boolean} + */ + abutsStart(other) { + if (!this.isValid) return false; + return +this.e === +other.s; + } + + /** + * Return whether this Interval's start is adjacent to the specified Interval's end. + * @param {Interval} other + * @return {boolean} + */ + abutsEnd(other) { + if (!this.isValid) return false; + return +other.e === +this.s; + } + + /** + * Return whether this Interval engulfs the start and end of the specified Interval. + * @param {Interval} other + * @return {boolean} + */ + engulfs(other) { + if (!this.isValid) return false; + return this.s <= other.s && this.e >= other.e; + } + + /** + * Return whether this Interval has the same start and end as the specified Interval. + * @param {Interval} other + * @return {boolean} + */ + equals(other) { + if (!this.isValid || !other.isValid) { + return false; + } + + return this.s.equals(other.s) && this.e.equals(other.e); + } + + /** + * Return an Interval representing the intersection of this Interval and the specified Interval. + * Specifically, the resulting Interval has the maximum start time and the minimum end time of the two Intervals. + * Returns null if the intersection is empty, meaning, the intervals don't intersect. + * @param {Interval} other + * @return {Interval} + */ + intersection(other) { + if (!this.isValid) return this; + const s = this.s > other.s ? this.s : other.s, + e = this.e < other.e ? this.e : other.e; + + if (s >= e) { + return null; + } else { + return Interval.fromDateTimes(s, e); + } + } + + /** + * Return an Interval representing the union of this Interval and the specified Interval. + * Specifically, the resulting Interval has the minimum start time and the maximum end time of the two Intervals. + * @param {Interval} other + * @return {Interval} + */ + union(other) { + if (!this.isValid) return this; + const s = this.s < other.s ? this.s : other.s, + e = this.e > other.e ? this.e : other.e; + return Interval.fromDateTimes(s, e); + } + + /** + * Merge an array of Intervals into a equivalent minimal set of Intervals. + * Combines overlapping and adjacent Intervals. + * @param {Array} intervals + * @return {Array} + */ + static merge(intervals) { + const [found, final] = intervals + .sort((a, b) => a.s - b.s) + .reduce( + ([sofar, current], item) => { + if (!current) { + return [sofar, item]; + } else if (current.overlaps(item) || current.abutsStart(item)) { + return [sofar, current.union(item)]; + } else { + return [sofar.concat([current]), item]; + } + }, + [[], null] + ); + if (final) { + found.push(final); + } + return found; + } + + /** + * Return an array of Intervals representing the spans of time that only appear in one of the specified Intervals. + * @param {Array} intervals + * @return {Array} + */ + static xor(intervals) { + let start = null, + currentCount = 0; + const results = [], + ends = intervals.map((i) => [ + { time: i.s, type: "s" }, + { time: i.e, type: "e" }, + ]), + flattened = Array.prototype.concat(...ends), + arr = flattened.sort((a, b) => a.time - b.time); + + for (const i of arr) { + currentCount += i.type === "s" ? 1 : -1; + + if (currentCount === 1) { + start = i.time; + } else { + if (start && +start !== +i.time) { + results.push(Interval.fromDateTimes(start, i.time)); + } + + start = null; + } + } + + return Interval.merge(results); + } + + /** + * Return an Interval representing the span of time in this Interval that doesn't overlap with any of the specified Intervals. + * @param {...Interval} intervals + * @return {Array} + */ + difference(...intervals) { + return Interval.xor([this].concat(intervals)) + .map((i) => this.intersection(i)) + .filter((i) => i && !i.isEmpty()); + } + + /** + * Returns a string representation of this Interval appropriate for debugging. + * @return {string} + */ + toString() { + if (!this.isValid) return INVALID; + return `[${this.s.toISO()} – ${this.e.toISO()})`; + } + + /** + * Returns a string representation of this Interval appropriate for the REPL. + * @return {string} + */ + [Symbol.for("nodejs.util.inspect.custom")]() { + if (this.isValid) { + return `Interval { start: ${this.s.toISO()}, end: ${this.e.toISO()} }`; + } else { + return `Interval { Invalid, reason: ${this.invalidReason} }`; + } + } + + /** + * Returns a localized string representing this Interval. Accepts the same options as the + * Intl.DateTimeFormat constructor and any presets defined by Luxon, such as + * {@link DateTime.DATE_FULL} or {@link DateTime.TIME_SIMPLE}. The exact behavior of this method + * is browser-specific, but in general it will return an appropriate representation of the + * Interval in the assigned locale. Defaults to the system's locale if no locale has been + * specified. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat + * @param {Object} [formatOpts=DateTime.DATE_SHORT] - Either a DateTime preset or + * Intl.DateTimeFormat constructor options. + * @param {Object} opts - Options to override the configuration of the start DateTime. + * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(); //=> 11/7/2022 – 11/8/2022 + * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL); //=> November 7 – 8, 2022 + * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL, { locale: 'fr-FR' }); //=> 7–8 novembre 2022 + * @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString(DateTime.TIME_SIMPLE); //=> 6:00 – 8:00 PM + * @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString({ weekday: 'short', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); //=> Mon, Nov 07, 6:00 – 8:00 p + * @return {string} + */ + toLocaleString(formatOpts = Formats.DATE_SHORT, opts = {}) { + return this.isValid + ? Formatter.create(this.s.loc.clone(opts), formatOpts).formatInterval(this) + : INVALID; + } + + /** + * Returns an ISO 8601-compliant string representation of this Interval. + * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals + * @param {Object} opts - The same options as {@link DateTime#toISO} + * @return {string} + */ + toISO(opts) { + if (!this.isValid) return INVALID; + return `${this.s.toISO(opts)}/${this.e.toISO(opts)}`; + } + + /** + * Returns an ISO 8601-compliant string representation of date of this Interval. + * The time components are ignored. + * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals + * @return {string} + */ + toISODate() { + if (!this.isValid) return INVALID; + return `${this.s.toISODate()}/${this.e.toISODate()}`; + } + + /** + * Returns an ISO 8601-compliant string representation of time of this Interval. + * The date components are ignored. + * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals + * @param {Object} opts - The same options as {@link DateTime#toISO} + * @return {string} + */ + toISOTime(opts) { + if (!this.isValid) return INVALID; + return `${this.s.toISOTime(opts)}/${this.e.toISOTime(opts)}`; + } + + /** + * Returns a string representation of this Interval formatted according to the specified format + * string. **You may not want this.** See {@link Interval#toLocaleString} for a more flexible + * formatting tool. + * @param {string} dateFormat - The format string. This string formats the start and end time. + * See {@link DateTime#toFormat} for details. + * @param {Object} opts - Options. + * @param {string} [opts.separator = ' – '] - A separator to place between the start and end + * representations. + * @return {string} + */ + toFormat(dateFormat, { separator = " – " } = {}) { + if (!this.isValid) return INVALID; + return `${this.s.toFormat(dateFormat)}${separator}${this.e.toFormat(dateFormat)}`; + } + + /** + * Return a Duration representing the time spanned by this interval. + * @param {string|string[]} [unit=['milliseconds']] - the unit or units (such as 'hours' or 'days') to include in the duration. + * @param {Object} opts - options that affect the creation of the Duration + * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use + * @example Interval.fromDateTimes(dt1, dt2).toDuration().toObject() //=> { milliseconds: 88489257 } + * @example Interval.fromDateTimes(dt1, dt2).toDuration('days').toObject() //=> { days: 1.0241812152777778 } + * @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes']).toObject() //=> { hours: 24, minutes: 34.82095 } + * @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes', 'seconds']).toObject() //=> { hours: 24, minutes: 34, seconds: 49.257 } + * @example Interval.fromDateTimes(dt1, dt2).toDuration('seconds').toObject() //=> { seconds: 88489.257 } + * @return {Duration} + */ + toDuration(unit, opts) { + if (!this.isValid) { + return Duration.invalid(this.invalidReason); + } + return this.e.diff(this.s, unit, opts); + } + + /** + * Run mapFn on the interval start and end, returning a new Interval from the resulting DateTimes + * @param {function} mapFn + * @return {Interval} + * @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.toUTC()) + * @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.plus({ hours: 2 })) + */ + mapEndpoints(mapFn) { + return Interval.fromDateTimes(mapFn(this.s), mapFn(this.e)); + } +} diff --git a/src/shared/libs/luxon/src/luxon.js b/src/shared/libs/luxon/src/luxon.js new file mode 100644 index 0000000..2c096b4 --- /dev/null +++ b/src/shared/libs/luxon/src/luxon.js @@ -0,0 +1,26 @@ +import DateTime from "./datetime.js"; +import Duration from "./duration.js"; +import Interval from "./interval.js"; +import Info from "./info.js"; +import Zone from "./zone.js"; +import FixedOffsetZone from "./zones/fixedOffsetZone.js"; +import IANAZone from "./zones/IANAZone.js"; +import InvalidZone from "./zones/invalidZone.js"; +import SystemZone from "./zones/systemZone.js"; +import Settings from "./settings.js"; + +const VERSION = "3.4.4"; + +export { + VERSION, + DateTime, + Duration, + Interval, + Info, + Zone, + FixedOffsetZone, + IANAZone, + InvalidZone, + SystemZone, + Settings, +}; diff --git a/src/shared/libs/luxon/src/package.json b/src/shared/libs/luxon/src/package.json new file mode 100644 index 0000000..befab6e --- /dev/null +++ b/src/shared/libs/luxon/src/package.json @@ -0,0 +1,4 @@ +{ + "type": "module", + "version": "3.4.4" +} diff --git a/src/shared/libs/luxon/src/settings.js b/src/shared/libs/luxon/src/settings.js new file mode 100644 index 0000000..462e71e --- /dev/null +++ b/src/shared/libs/luxon/src/settings.js @@ -0,0 +1,175 @@ +import SystemZone from "./zones/systemZone.js"; +import IANAZone from "./zones/IANAZone.js"; +import Locale from "./impl/locale.js"; + +import { normalizeZone } from "./impl/zoneUtil.js"; +import { validateWeekSettings } from "./impl/util.js"; + +let now = () => Date.now(), + defaultZone = "system", + defaultLocale = null, + defaultNumberingSystem = null, + defaultOutputCalendar = null, + twoDigitCutoffYear = 60, + throwOnInvalid, + defaultWeekSettings = null; + +/** + * Settings contains static getters and setters that control Luxon's overall behavior. Luxon is a simple library with few options, but the ones it does have live here. + */ +export default class Settings { + /** + * Get the callback for returning the current timestamp. + * @type {function} + */ + static get now() { + return now; + } + + /** + * Set the callback for returning the current timestamp. + * The function should return a number, which will be interpreted as an Epoch millisecond count + * @type {function} + * @example Settings.now = () => Date.now() + 3000 // pretend it is 3 seconds in the future + * @example Settings.now = () => 0 // always pretend it's Jan 1, 1970 at midnight in UTC time + */ + static set now(n) { + now = n; + } + + /** + * Set the default time zone to create DateTimes in. Does not affect existing instances. + * Use the value "system" to reset this value to the system's time zone. + * @type {string} + */ + static set defaultZone(zone) { + defaultZone = zone; + } + + /** + * Get the default time zone object currently used to create DateTimes. Does not affect existing instances. + * The default value is the system's time zone (the one set on the machine that runs this code). + * @type {Zone} + */ + static get defaultZone() { + return normalizeZone(defaultZone, SystemZone.instance); + } + + /** + * Get the default locale to create DateTimes with. Does not affect existing instances. + * @type {string} + */ + static get defaultLocale() { + return defaultLocale; + } + + /** + * Set the default locale to create DateTimes with. Does not affect existing instances. + * @type {string} + */ + static set defaultLocale(locale) { + defaultLocale = locale; + } + + /** + * Get the default numbering system to create DateTimes with. Does not affect existing instances. + * @type {string} + */ + static get defaultNumberingSystem() { + return defaultNumberingSystem; + } + + /** + * Set the default numbering system to create DateTimes with. Does not affect existing instances. + * @type {string} + */ + static set defaultNumberingSystem(numberingSystem) { + defaultNumberingSystem = numberingSystem; + } + + /** + * Get the default output calendar to create DateTimes with. Does not affect existing instances. + * @type {string} + */ + static get defaultOutputCalendar() { + return defaultOutputCalendar; + } + + /** + * Set the default output calendar to create DateTimes with. Does not affect existing instances. + * @type {string} + */ + static set defaultOutputCalendar(outputCalendar) { + defaultOutputCalendar = outputCalendar; + } + + /** + * @typedef {Object} WeekSettings + * @property {number} firstDay + * @property {number} minimalDays + * @property {number[]} weekend + */ + + /** + * @return {WeekSettings|null} + */ + static get defaultWeekSettings() { + return defaultWeekSettings; + } + + /** + * Allows overriding the default locale week settings, i.e. the start of the week, the weekend and + * how many days are required in the first week of a year. + * Does not affect existing instances. + * + * @param {WeekSettings|null} weekSettings + */ + static set defaultWeekSettings(weekSettings) { + defaultWeekSettings = validateWeekSettings(weekSettings); + } + + /** + * Get the cutoff year after which a string encoding a year as two digits is interpreted to occur in the current century. + * @type {number} + */ + static get twoDigitCutoffYear() { + return twoDigitCutoffYear; + } + + /** + * Set the cutoff year after which a string encoding a year as two digits is interpreted to occur in the current century. + * @type {number} + * @example Settings.twoDigitCutoffYear = 0 // cut-off year is 0, so all 'yy' are interpreted as current century + * @example Settings.twoDigitCutoffYear = 50 // '49' -> 1949; '50' -> 2050 + * @example Settings.twoDigitCutoffYear = 1950 // interpreted as 50 + * @example Settings.twoDigitCutoffYear = 2050 // ALSO interpreted as 50 + */ + static set twoDigitCutoffYear(cutoffYear) { + twoDigitCutoffYear = cutoffYear % 100; + } + + /** + * Get whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals + * @type {boolean} + */ + static get throwOnInvalid() { + return throwOnInvalid; + } + + /** + * Set whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals + * @type {boolean} + */ + static set throwOnInvalid(t) { + throwOnInvalid = t; + } + + /** + * Reset Luxon's global caches. Should only be necessary in testing scenarios. + * @return {void} + */ + static resetCaches() { + Locale.resetCache(); + IANAZone.resetCache(); + } +} diff --git a/src/shared/libs/luxon/src/zone.js b/src/shared/libs/luxon/src/zone.js new file mode 100644 index 0000000..cec0e4f --- /dev/null +++ b/src/shared/libs/luxon/src/zone.js @@ -0,0 +1,91 @@ +import { ZoneIsAbstractError } from "./errors.js"; + +/** + * @interface + */ +export default class Zone { + /** + * The type of zone + * @abstract + * @type {string} + */ + get type() { + throw new ZoneIsAbstractError(); + } + + /** + * The name of this zone. + * @abstract + * @type {string} + */ + get name() { + throw new ZoneIsAbstractError(); + } + + get ianaName() { + return this.name; + } + + /** + * Returns whether the offset is known to be fixed for the whole year. + * @abstract + * @type {boolean} + */ + get isUniversal() { + throw new ZoneIsAbstractError(); + } + + /** + * Returns the offset's common name (such as EST) at the specified timestamp + * @abstract + * @param {number} ts - Epoch milliseconds for which to get the name + * @param {Object} opts - Options to affect the format + * @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'. + * @param {string} opts.locale - What locale to return the offset name in. + * @return {string} + */ + offsetName(ts, opts) { + throw new ZoneIsAbstractError(); + } + + /** + * Returns the offset's value as a string + * @abstract + * @param {number} ts - Epoch milliseconds for which to get the offset + * @param {string} format - What style of offset to return. + * Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively + * @return {string} + */ + formatOffset(ts, format) { + throw new ZoneIsAbstractError(); + } + + /** + * Return the offset in minutes for this zone at the specified timestamp. + * @abstract + * @param {number} ts - Epoch milliseconds for which to compute the offset + * @return {number} + */ + offset(ts) { + throw new ZoneIsAbstractError(); + } + + /** + * Return whether this Zone is equal to another zone + * @abstract + * @param {Zone} otherZone - the zone to compare + * @return {boolean} + */ + equals(otherZone) { + throw new ZoneIsAbstractError(); + } + + /** + * Return whether this Zone is valid. + * @abstract + * @type {boolean} + */ + get isValid() { + throw new ZoneIsAbstractError(); + } +} diff --git a/src/shared/libs/luxon/src/zones/IANAZone.js b/src/shared/libs/luxon/src/zones/IANAZone.js new file mode 100644 index 0000000..ad59451 --- /dev/null +++ b/src/shared/libs/luxon/src/zones/IANAZone.js @@ -0,0 +1,189 @@ +import { formatOffset, parseZoneInfo, isUndefined, objToLocalTS } from "../impl/util.js"; +import Zone from "../zone.js"; + +let dtfCache = {}; +function makeDTF(zone) { + if (!dtfCache[zone]) { + dtfCache[zone] = new Intl.DateTimeFormat("en-US", { + hour12: false, + timeZone: zone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + era: "short", + }); + } + return dtfCache[zone]; +} + +const typeToPos = { + year: 0, + month: 1, + day: 2, + era: 3, + hour: 4, + minute: 5, + second: 6, +}; + +function hackyOffset(dtf, date) { + const formatted = dtf.format(date).replace(/\u200E/g, ""), + parsed = /(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(formatted), + [, fMonth, fDay, fYear, fadOrBc, fHour, fMinute, fSecond] = parsed; + return [fYear, fMonth, fDay, fadOrBc, fHour, fMinute, fSecond]; +} + +function partsOffset(dtf, date) { + const formatted = dtf.formatToParts(date); + const filled = []; + for (let i = 0; i < formatted.length; i++) { + const { type, value } = formatted[i]; + const pos = typeToPos[type]; + + if (type === "era") { + filled[pos] = value; + } else if (!isUndefined(pos)) { + filled[pos] = parseInt(value, 10); + } + } + return filled; +} + +let ianaZoneCache = {}; +/** + * A zone identified by an IANA identifier, like America/New_York + * @implements {Zone} + */ +export default class IANAZone extends Zone { + /** + * @param {string} name - Zone name + * @return {IANAZone} + */ + static create(name) { + if (!ianaZoneCache[name]) { + ianaZoneCache[name] = new IANAZone(name); + } + return ianaZoneCache[name]; + } + + /** + * Reset local caches. Should only be necessary in testing scenarios. + * @return {void} + */ + static resetCache() { + ianaZoneCache = {}; + dtfCache = {}; + } + + /** + * Returns whether the provided string is a valid specifier. This only checks the string's format, not that the specifier identifies a known zone; see isValidZone for that. + * @param {string} s - The string to check validity on + * @example IANAZone.isValidSpecifier("America/New_York") //=> true + * @example IANAZone.isValidSpecifier("Sport~~blorp") //=> false + * @deprecated This method returns false for some valid IANA names. Use isValidZone instead. + * @return {boolean} + */ + static isValidSpecifier(s) { + return this.isValidZone(s); + } + + /** + * Returns whether the provided string identifies a real zone + * @param {string} zone - The string to check + * @example IANAZone.isValidZone("America/New_York") //=> true + * @example IANAZone.isValidZone("Fantasia/Castle") //=> false + * @example IANAZone.isValidZone("Sport~~blorp") //=> false + * @return {boolean} + */ + static isValidZone(zone) { + if (!zone) { + return false; + } + try { + new Intl.DateTimeFormat("en-US", { timeZone: zone }).format(); + return true; + } catch (e) { + return false; + } + } + + constructor(name) { + super(); + /** @private **/ + this.zoneName = name; + /** @private **/ + this.valid = IANAZone.isValidZone(name); + } + + /** @override **/ + get type() { + return "iana"; + } + + /** @override **/ + get name() { + return this.zoneName; + } + + /** @override **/ + get isUniversal() { + return false; + } + + /** @override **/ + offsetName(ts, { format, locale }) { + return parseZoneInfo(ts, format, locale, this.name); + } + + /** @override **/ + formatOffset(ts, format) { + return formatOffset(this.offset(ts), format); + } + + /** @override **/ + offset(ts) { + const date = new Date(ts); + + if (isNaN(date)) return NaN; + + const dtf = makeDTF(this.name); + let [year, month, day, adOrBc, hour, minute, second] = dtf.formatToParts + ? partsOffset(dtf, date) + : hackyOffset(dtf, date); + + if (adOrBc === "BC") { + year = -Math.abs(year) + 1; + } + + // because we're using hour12 and https://bugs.chromium.org/p/chromium/issues/detail?id=1025564&can=2&q=%2224%3A00%22%20datetimeformat + const adjustedHour = hour === 24 ? 0 : hour; + + const asUTC = objToLocalTS({ + year, + month, + day, + hour: adjustedHour, + minute, + second, + millisecond: 0, + }); + + let asTS = +date; + const over = asTS % 1000; + asTS -= over >= 0 ? over : 1000 + over; + return (asUTC - asTS) / (60 * 1000); + } + + /** @override **/ + equals(otherZone) { + return otherZone.type === "iana" && otherZone.name === this.name; + } + + /** @override **/ + get isValid() { + return this.valid; + } +} diff --git a/src/shared/libs/luxon/src/zones/fixedOffsetZone.js b/src/shared/libs/luxon/src/zones/fixedOffsetZone.js new file mode 100644 index 0000000..dcfa25a --- /dev/null +++ b/src/shared/libs/luxon/src/zones/fixedOffsetZone.js @@ -0,0 +1,102 @@ +import { formatOffset, signedOffset } from "../impl/util.js"; +import Zone from "../zone.js"; + +let singleton = null; + +/** + * A zone with a fixed offset (meaning no DST) + * @implements {Zone} + */ +export default class FixedOffsetZone extends Zone { + /** + * Get a singleton instance of UTC + * @return {FixedOffsetZone} + */ + static get utcInstance() { + if (singleton === null) { + singleton = new FixedOffsetZone(0); + } + return singleton; + } + + /** + * Get an instance with a specified offset + * @param {number} offset - The offset in minutes + * @return {FixedOffsetZone} + */ + static instance(offset) { + return offset === 0 ? FixedOffsetZone.utcInstance : new FixedOffsetZone(offset); + } + + /** + * Get an instance of FixedOffsetZone from a UTC offset string, like "UTC+6" + * @param {string} s - The offset string to parse + * @example FixedOffsetZone.parseSpecifier("UTC+6") + * @example FixedOffsetZone.parseSpecifier("UTC+06") + * @example FixedOffsetZone.parseSpecifier("UTC-6:00") + * @return {FixedOffsetZone} + */ + static parseSpecifier(s) { + if (s) { + const r = s.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i); + if (r) { + return new FixedOffsetZone(signedOffset(r[1], r[2])); + } + } + return null; + } + + constructor(offset) { + super(); + /** @private **/ + this.fixed = offset; + } + + /** @override **/ + get type() { + return "fixed"; + } + + /** @override **/ + get name() { + return this.fixed === 0 ? "UTC" : `UTC${formatOffset(this.fixed, "narrow")}`; + } + + get ianaName() { + if (this.fixed === 0) { + return "Etc/UTC"; + } else { + return `Etc/GMT${formatOffset(-this.fixed, "narrow")}`; + } + } + + /** @override **/ + offsetName() { + return this.name; + } + + /** @override **/ + formatOffset(ts, format) { + return formatOffset(this.fixed, format); + } + + /** @override **/ + get isUniversal() { + return true; + } + + /** @override **/ + offset() { + return this.fixed; + } + + /** @override **/ + equals(otherZone) { + return otherZone.type === "fixed" && otherZone.fixed === this.fixed; + } + + /** @override **/ + get isValid() { + return true; + } +} diff --git a/src/shared/libs/luxon/src/zones/invalidZone.js b/src/shared/libs/luxon/src/zones/invalidZone.js new file mode 100644 index 0000000..9a1a2d4 --- /dev/null +++ b/src/shared/libs/luxon/src/zones/invalidZone.js @@ -0,0 +1,53 @@ +import Zone from "../zone.js"; + +/** + * A zone that failed to parse. You should never need to instantiate this. + * @implements {Zone} + */ +export default class InvalidZone extends Zone { + constructor(zoneName) { + super(); + /** @private */ + this.zoneName = zoneName; + } + + /** @override **/ + get type() { + return "invalid"; + } + + /** @override **/ + get name() { + return this.zoneName; + } + + /** @override **/ + get isUniversal() { + return false; + } + + /** @override **/ + offsetName() { + return null; + } + + /** @override **/ + formatOffset() { + return ""; + } + + /** @override **/ + offset() { + return NaN; + } + + /** @override **/ + equals() { + return false; + } + + /** @override **/ + get isValid() { + return false; + } +} diff --git a/src/shared/libs/luxon/src/zones/systemZone.js b/src/shared/libs/luxon/src/zones/systemZone.js new file mode 100644 index 0000000..533e663 --- /dev/null +++ b/src/shared/libs/luxon/src/zones/systemZone.js @@ -0,0 +1,61 @@ +import { formatOffset, parseZoneInfo } from "../impl/util.js"; +import Zone from "../zone.js"; + +let singleton = null; + +/** + * Represents the local zone for this JavaScript environment. + * @implements {Zone} + */ +export default class SystemZone extends Zone { + /** + * Get a singleton instance of the local zone + * @return {SystemZone} + */ + static get instance() { + if (singleton === null) { + singleton = new SystemZone(); + } + return singleton; + } + + /** @override **/ + get type() { + return "system"; + } + + /** @override **/ + get name() { + return new Intl.DateTimeFormat().resolvedOptions().timeZone; + } + + /** @override **/ + get isUniversal() { + return false; + } + + /** @override **/ + offsetName(ts, { format, locale }) { + return parseZoneInfo(ts, format, locale); + } + + /** @override **/ + formatOffset(ts, format) { + return formatOffset(this.offset(ts), format); + } + + /** @override **/ + offset(ts) { + return -new Date(ts).getTimezoneOffset(); + } + + /** @override **/ + equals(otherZone) { + return otherZone.type === "system"; + } + + /** @override **/ + get isValid() { + return true; + } +} diff --git a/src/shared/modals/InteractiveData/InteractiveElements/DateTimeElement.ts b/src/shared/modals/InteractiveData/InteractiveElements/DateTimeElement.ts new file mode 100644 index 0000000..817a9dd --- /dev/null +++ b/src/shared/modals/InteractiveData/InteractiveElements/DateTimeElement.ts @@ -0,0 +1,103 @@ +import { BaseInputElement } from "./index"; + +import { ElementType } from "../../../constants/UIKitConstants"; + +/** + * Represents a DateTime input element. + */ +export class DateTimeElement extends BaseInputElement<string> { + /** + * The label of the input element. + */ + private label: string; + private mode: string; + private timezoneCode: string; + private to: string; + private from: string; + private dateTimeFormat: string; + /** + * The default value to be selected in the DateTime. + */ + private defaultValue?: string; + + /** + * Creates a new instance of DateTimeInput. + * @param elementId - The ID of the input element. + * @param label - The label of the input element. + * @param options - The options available for the DateTime input element. + */ + constructor(elementId: string, json) { + super(elementId, ElementType.dateTime); + Object.assign(this, json); + } + + getTimeZone(): string { + return this.timezoneCode; + } + + setTimeZone(timezoneCode: string): string { + return (this.timezoneCode = timezoneCode); + } + + getMode(): string { + return this.mode; + } + + setMode(mode: string): string { + return (this.mode = mode); + } + + getToDateTime(): string { + return this.to; + } + + setToDateTime(to: string): string { + return (this.to = to); + } + + getFromDateTime(): string { + return this.from; + } + + setFromDateTime(from: string): string { + return (this.from = from); + } + + getDateTimeFormat(): string { + return this.dateTimeFormat; + } + + setDateTimeFormat(dateTimeFormat: string): string { + return (this.dateTimeFormat = dateTimeFormat); + } + + /** + * Gets the label of the input element. + * @returns The label of the input element. + */ + getLabel(): string { + return this.label; + } + + /** + * Gets the default value to be selected in the DateTime. + * @returns The default value to be selected in the DateTime. + */ + getDefaultValue(): string | undefined { + return this.defaultValue; + } + + /** + * Sets the default value to be selected in the DateTime. + * @param defaultValue - The default value to be selected in the DateTime. + */ + setDefaultValue(defaultValue: string): void { + this.defaultValue = defaultValue; + } + + static fromJSON(json: any): DateTimeElement { + const dateTimeElement = new DateTimeElement(json.elementId, json); + if (json.defaultValue) dateTimeElement.setDefaultValue(json.defaultValue); + return dateTimeElement; + } +} diff --git a/src/shared/modals/InteractiveData/InteractiveEntities/ElementEntity.ts b/src/shared/modals/InteractiveData/InteractiveEntities/ElementEntity.ts index 74a77c9..aa4e937 100644 --- a/src/shared/modals/InteractiveData/InteractiveEntities/ElementEntity.ts +++ b/src/shared/modals/InteractiveData/InteractiveEntities/ElementEntity.ts @@ -1,6 +1,7 @@ import { BaseInputElement, BaseInteractiveElement, LabelElement } from "../InteractiveElements"; import { ElementType } from "../../../constants/UIKitConstants"; +import { DateTimeElement } from "../InteractiveElements/DateTimeElement"; /** * Represents the base input for a dynamic form element. @@ -52,7 +53,9 @@ export class ElementEntity { case ElementType.button: return BaseInteractiveElement.fromJSON(json); case ElementType.label: - return LabelElement.fromJSON(json); + return LabelElement.fromJSON(json); + case ElementType.dateTime: + return DateTimeElement.fromJSON(json); default: return LabelElement.fromJSON({ elementId: "1", diff --git a/src/shared/modals/InteractiveData/InteractiveMessage/SchedulerMessage.ts b/src/shared/modals/InteractiveData/InteractiveMessage/SchedulerMessage.ts new file mode 100644 index 0000000..2a3cc81 --- /dev/null +++ b/src/shared/modals/InteractiveData/InteractiveMessage/SchedulerMessage.ts @@ -0,0 +1,149 @@ +import { ButtonElement } from "../InteractiveElements/index"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; +import { MessageTypeConstants } from "../../../constants/UIKitConstants"; + +interface TimeRange { + from: string; + to: string; +} + +interface Availability { + [key: string]: TimeRange[]; +} + +interface InteractiveData { + title: string; + avatarUrl: string; + goalCompletionText: string; + timezoneCode: string; + bufferTime: number; + duration: number; + availability: Availability; + dateRangeStart: number; + dateRangeEnd: number; + icsFileUrl: string; + scheduleElement: ButtonElement; +} + +export class SchedulerMessage extends CometChat.InteractiveMessage { + private interactiveData: InteractiveData; + private title: string; + private avatarUrl: string; + private goalCompletionText: string; + private timezoneCode: string; + private bufferTime: number; + private duration: number; + private availability: Availability; + private dateRangeStart: number; + private dateRangeEnd: number; + private icsFileUrl: string; + private scheduleElement: ButtonElement; + + constructor( + receiverId: string, + receiverType: string, + interactiveData: InteractiveData + ) { + super( + receiverId, + receiverType, + MessageTypeConstants.scheduler, + interactiveData + ); + this.interactiveData = interactiveData; + Object.assign(this, interactiveData); + } + + // Setters + setInteractiveData(interactiveData: InteractiveData) { + this.interactiveData = interactiveData; + } + setTitle(title: string) { + this.title = title; + } + setAvatarUrl(avatarUrl: string) { + this.avatarUrl = avatarUrl; + } + setGoalCompletionText(goalCompletionText: string) { + this.goalCompletionText = goalCompletionText; + } + setTimezoneCode(timezoneCode: string) { + this.timezoneCode = timezoneCode; + } + setduration(duration: number) { + this.duration = duration; + } + setBufferTime(bufferTime: number) { + this.bufferTime = bufferTime; + } + setAvailability(availability: Availability) { + this.availability = availability; + } + setDateRangeStart(dateRangeStart: number) { + this.dateRangeStart = dateRangeStart; + } + setdateRangeEnd(dateRangeEnd: number) { + this.dateRangeEnd = dateRangeEnd; + } + setIcsFileUrl(icsFileUrl: string) { + this.icsFileUrl = icsFileUrl; + } + setScheduleElement(scheduleElement: ButtonElement) { + this.scheduleElement = ButtonElement.fromJSON(scheduleElement); + } + + // ... more setters as needed + + // Getters + getInteractiveData() { + return this.interactiveData; + } + getTitle() { + return this.title; + } + getAvatarUrl() { + return this.avatarUrl; + } + getGoalCompletionText() { + return this.goalCompletionText; + } + getTimezoneCode() { + return this.timezoneCode; + } + getduration() { + return this.duration; + } + getBufferTime() { + return this.bufferTime; + } + getAvailability() { + return this.availability; + } + getdateRangeStart() { + return this.dateRangeStart; + } + getdateRangeEnd() { + return this.dateRangeEnd; + } + getIcsFileUrl() { + return this.icsFileUrl; + } + getScheduleElement() { + return this.scheduleElement; + } + + // ... more getters as needed + + // Method to refresh the data in the parent class + + static fromJSON(json: any): SchedulerMessage { + let interactiveData = json.data.interactiveData; + const schedulerMessage = new SchedulerMessage( + json.receiverId, + json.receiverType, + interactiveData + ); + Object.assign(schedulerMessage, { ...json, ...interactiveData }); + return schedulerMessage; + } +} diff --git a/src/shared/modals/InteractiveData/InteractiveMessage/index.ts b/src/shared/modals/InteractiveData/InteractiveMessage/index.ts index 6a0ffeb..30d270a 100644 --- a/src/shared/modals/InteractiveData/InteractiveMessage/index.ts +++ b/src/shared/modals/InteractiveData/InteractiveMessage/index.ts @@ -1,3 +1,4 @@ export { FormMessage } from "./FormMessage"; export { CardMessage } from "./CardMessage"; export { CustomInteractiveMessage } from "./CustomInteractiveMessage"; +export { SchedulerMessage } from "./SchedulerMessage"; diff --git a/src/shared/modals/InteractiveData/index.ts b/src/shared/modals/InteractiveData/index.ts index 75307f5..54d5ef0 100644 --- a/src/shared/modals/InteractiveData/index.ts +++ b/src/shared/modals/InteractiveData/index.ts @@ -3,7 +3,8 @@ export { FormMessage, CustomInteractiveMessage, - CardMessage + CardMessage, + SchedulerMessage } from "./InteractiveMessage"; export { URLNavigationAction, diff --git a/src/shared/modals/index.ts b/src/shared/modals/index.ts index 9802602..11dae8d 100644 --- a/src/shared/modals/index.ts +++ b/src/shared/modals/index.ts @@ -5,7 +5,7 @@ import { CometChatDetailsTemplate } from './CometChatDetailsTemplate'; import { CometChatDetailsOption } from './CometChatDetailsOption'; import { CometChatCallLogDetailsTemplate } from './CometChatCallLogDetailsTemplate'; import { CometChatCallLogDetailsOption } from './CometChatCallLogDetailsOptions'; -import { APIAction, ActionEntity, BaseInputElement, BaseInteractiveElement, ButtonElement, CardMessage, CheckboxElement, CustomAction, CustomInteractiveMessage, DropdownElement, ElementEntity, FormMessage, LabelElement, OptionElement, RadioButtonElement, SingleSelectElement, TextInputElement, URLNavigationAction } from './InteractiveData' +import { APIAction, ActionEntity, BaseInputElement, BaseInteractiveElement, ButtonElement, CardMessage, CheckboxElement, CustomAction, CustomInteractiveMessage, DropdownElement, ElementEntity, FormMessage, LabelElement, OptionElement, RadioButtonElement, SingleSelectElement, TextInputElement, URLNavigationAction, SchedulerMessage } from './InteractiveData' export { CometChatOptions, @@ -32,5 +32,6 @@ export { TextInputElement, URLNavigationAction, CometChatCallLogDetailsTemplate, - CometChatCallLogDetailsOption + CometChatCallLogDetailsOption, + SchedulerMessage }; diff --git a/src/shared/resources/CometChatLocalize/resources/ar/translation.json b/src/shared/resources/CometChatLocalize/resources/ar/translation.json index a78d53e..5abef9e 100644 --- a/src/shared/resources/CometChatLocalize/resources/ar/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/ar/translation.json @@ -1,292 +1,302 @@ { - "USERS": "المستخدمون", - "CHATS": "دردشات", - "GROUPS": "المجموعات", - "MORE": "المزيد", - "MESSAGE_IMAGE": "📷 صورة", - "MESSAGE_FILE": "📁 ملف", - "MESSAGE_VIDEO": "📹 فيديو", - "MESSAGE_AUDIO": "🎵 الصوت", - "CUSTOM_MESSAGE": "لديك رسالة", - "MISSED_VOICE_CALL": "مكالمة صوتية غاب", - "MISSED_VIDEO_CALL": "مكالمة فيديو غاب", - "CUSTOM_MESSAGE_POLL": "📊 استطلاع للرأي", - "CUSTOM_MESSAGE_STICKER": "💟 ملصق", - "CUSTOM_MESSAGE_DOCUMENT": "📃 وثيقة", - "NO_REPLIES_FOUND":"لم يتم العثور على أي ردود", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 السبورة", - "ONLINE": "عبر الإنترنت", - "ADMINISTRATOR": "مدير", - "MODERATOR": "مدير الجلسة", - "SUGGEST_A_REPLY":"اقترح ردًا", - "GENERATIONG_ICEBREAKER":"توليد كاسحات الجليد", - "GENERATING_REPLIES":"توليد الردود", - "PARTICIPANT": "مشارك", - "PUBLIC": "عامة", - "PRIVATE": "خاص", - "PASSWORD_PROTECTED": "محمية بكلمة مرور", - "PRIVACY_AND_SECURITY": "الخصوصية والأمان", - "PREFERENCES": "التفضيلات", - "MEMBERS": "الأعضاء", - "TODAY": "اليوم", - "YESTERDAY": "البارحة", - "SUNDAY": "الأحد", - "MONDAY": "الإثنين", - "TUESDAY": "الثلاثاء", - "WEDNESDAY": "الأربعاء", - "THURSDAY": "الخميس", - "FRIDAY": "الجمعة", - "SATURDAY": "يوم السبت", - "TYPING": "كتابة...", - "IS_TYPING": "هو كتابة...", - "CLOSE": "إغلاق", - "ENTER_GROUP_NAME": "أدخل اسم المجموعة", - "ADD_MEMBERS": "إضافة أعضاء", - "SEND_MESSAGE": "ارسل رسالة", - "UNBLOCK_USER": "إلغاء حظر المستخدم", - "BLOCK_USER": "كتلة المستخدم", - "DELETE_AND_EXIT": "حذف وخروج", - "LEAVE_GROUP": "ترك المجموعة", - "CREATE_GROUP": "إنشاء مجموعة", - "SHARED_MEDIA": "وسائل الإعلام المشتركة", - "VIDEO_CALL": "مكالمة فيديو", - "AUDIO_CALL": "مكالمة صوتية", - "LOADING": "التحميل...", - "REPLY": "الرد", - "REPLIES": "الردود", - "LAUNCH": "إطلاق", - "SHARED_COLLABORATIVE_DOCUMENT": "وقد شارك وثيقة تعاونية", - "SHARED_COLLABORATIVE_WHITEBOARD": "مشاركة السبورة التعاونية", - "CREATED_WHITEBOARD": "لقد قمت بإنشاء لوحة بيضاء تعاونية جديدة", - "CREATED_DOCUMENT": "لقد قمت بإنشاء مستند تعاوني جديد", - "PHOTOS": "صور", - "VIDEOS": "فيديوهات", - "DOCUMENT": "مستند", - "YOU_DELETED_THIS_MESSAGE": "⚠️ قمت بحذف هذه الرسالة", - "THIS_MESSAGE_DELETED": "⚠️ تم حذف هذه الرسالة", - "VIEW_ON_YOUTUBE": "عرض على يوتيوب", - "SEARCH": "البحث", - "NO_USERS_FOUND": "لم يتم العثور على المستخدمين", - "ERROR": "خطأ", - "NO_GROUPS_FOUND": "لم يتم العثور على مجموعات", - "NO_CHATS_FOUND": "لم يتم العثور على دردشات", - "MEDIA_MESSAGE": "رسالة إعلامية", - "INCOMING_AUDIO_CALL": "مكالمة صوتية واردة", - "INCOMING_VIDEO_CALL": "مكالمة فيديو واردة", - "DECLINE": "انخفاض", - "ACCEPT": "قبول", - "CALL_INITIATED": "بدأ الاتصال", - "OUTGOING_AUDIO_CALL": "مكالمة صوتية صادرة", - "OUTGOING_VIDEO_CALL": "مكالمة فيديو صادرة", - "CALL_REJECTED": "تم رفض المكالمة", - "REJECTED_CALL": "مكالمة مرفوضة", - "CALL_ACCEPTED": "تم قبول المكالمة", - "JOINED": "انضم", - "LEFT_THE_CALL": "ترك المكالمة", - "UNANSWERED_AUDIO_CALL": "مكالمة صوتية لم تتم الإجابة عليها", - "UNANSWERED_VIDEO_CALL": "مكالمة فيديو لم تتم الإجابة عليها", - "CALL_ENDED": "انتهت المكالمة", - "CANCELLED_CALL": "مكالمة ملغاة", - "CALL_BUSY": "استدعاء مشغول", - "CALLING": "الدعوة...", - "ADD": "إضافة", - "NO_BANNED_MEMBERS_FOUND": "لم يتم العثور على أعضاء محظورين", - "BANNED_MEMBERS": "الأعضاء المحظورين", - "NAME": "اسم", - "SCOPE": "النطاق", - "UNBAN": "نبان", - "SELECT_GROUP_TYPE": "تحديد نوع المجموعة", - "ENTER_GROUP_PASSWORD": "أدخل كلمة مرور المجموعة", - "CREATE": "إنشاء", - "CREATE_POLL": "إنشاء استطلاع للرأي", - "QUESTION": "سؤال", - "ENTER_YOUR_QUESTION": "أدخل سؤالك", - "OPTIONS": "خيارات", - "ENTER_YOUR_OPTION": "أدخل الخيار الخاص بك", - "ADD_NEW_OPTION": "إضافة خيار جديد", - "VIEW_MEMBERS": "عرض الأعضاء", - "DETAILS": "تفاصيل", - "NOTIFICATIONS": "الإشعارات", - "OTHER": "أخرى", - "HELP": "مساعدة", - "REPORT_PROBLEM": "الإبلاغ عن مشكلة", - "GROUP_MEMBERS": "أعضاء المجموعة", - "BAN": "بان", - "KICK": "ركلة", - "PICK_YOUR_EMOJI": "اختيار الرموز التعبيرية الخاصة بك", - "PRIVATE_GROUP": "مجموعة خاصة", - "PROTECTED_GROUP": "المجموعة المحمية", - "VISIT": "زيارة", - "ATTACH": "إرفاق", - "ATTACH_FILE": "إرفاق ملف", - "ATTACH_VIDEO": "إرفاق الفيديو", - "ATTACH_AUDIO": "إرفاق الصوت", - "ATTACH_IMAGE": "إرفاق صورة", - "COLLABORATE_USING_DOCUMENT": "التعاون باستخدام مستند", - "COLLABORATE_USING_WHITEBOARD": "التعاون باستخدام لوح معلومات", - "EMOJI": "رمز تعبيري", - "ENTER_YOUR_MESSAGE_HERE": "أدخل رسالتك هنا", - "NO_MESSAGES_FOUND": "لم يتم العثور على رسائل", - "THREAD": "الموضوع", - "COLLABORATIVE_DOCUMENT": "وثيقة تعاونية", - "COLLABORATIVE_WHITEBOARD": "السبورة التعاونية", - "ADD_REACTION": "إضافة رد فعل", - "NO_STICKERS_FOUND": "لم يتم العثور على ملصقات", - "REPLY_TO_THREAD": "الرد على موضوع", - "REPLY_IN_THREAD": "الرد في موضوع", - "DELETE_MESSAGE": "حذف الرسالة", - "EDIT_MESSAGE": "تحرير الرسالة", - "OWNER": "مالك", - "CHANGE_SCOPE": "تغيير النطاق", - "STICKER": "ملصق", - "LAST_ACTIVE_AT": "آخر نشط في", - "VOICE_CALL": "مكالمة صوتية", - "VIEW_DETAIL": "عرض التفاصيل", - "VOTES": "التصويت", - "VOTE": "تصويت", - "NO_VOTE": "لا تصويت", - "REACTED": "رد فعل", - "ADDED": "أضاف", - "UNBANNED": "غير محظور", - "MADE": "صنع", - "UNANSWERED_CALL": "مكالمة لم يتم الرد عليها", - "MISSED_AUDIO_CALL": "مكالمة صوتية فائتة", - "ENTER_YOUR_PASSWORD": "أدخل كلمة المرور", - "DOCS": "مستندات", - "NO_RECORDS_FOUND": "لم يتم العثور على سجلات", - "LIVE_REACTION": "رد فعل حي", - "SMILEY_PEOPLE": "الوجوه الضاحكة والناس", - "ANIMALES_NATURE": "الحيوانات والطبيعة", - "FOOD_DRINK": "الطعام والشراب", - "ACTIVITY": "النشاط", - "TRAVEL_PLACES": "السفر والأماكن", - "OBJECTS": "كائنات", - "SYMBOLS": "الرموز", - "FLAGS": "أعلام", - "SENT": "أرسلت", - "SEEN": "شاهد", - "DELIVERED": "سلمت", - "TRANSLATE_MESSAGE": "ترجمة الرسالة", - "LEFT": "يسار", - "KICKED": "ركل", - "BANNED": "محظور", - "NEW_MESSAGES": "رسائل جديدة", - "NEW_MESSAGE": "رسالة جديدة", - "JUMP": "القفز", - "SELECT_VIDEO_SOURCE": "حدد مصدر الفيديو", - "SELECT_INPUT_AUDIO_SOURCE": "حدد مصدر صوت الإدخال", - "SELECT_OUTPUT_AUDIO_SOURCE": "حدد مصدر الصوت الناتج", - "INITIATED_GROUP_CALL": "بدأت مكالمة جماعية", - "YOU_INITIATED_GROUP_CALL": "لقد بدأت مكالمة جماعية", - "IGNORE": "تجاهل", - "ON_ANOTHER_CALL": "هو على مكالمة أخرى", - "CREATING": "خلق", - "AVATAR": "أفاتار", - "GROUP_NAME_BLANK": "اسم المجموعة لا يمكن أن تكون فارغة", - "GROUP_TYPE_BLANK": "نوع المجموعة لا يمكن أن تكون فارغة", - "GROUP_PASSWORD_BLANK": "كلمة مرور المجموعة لا يمكن أن تكون فارغة", - "POLL_QUESTION_BLANK": "السؤال لا يمكن أن تكون فارغة", - "POLL_OPTION_BLANK": "الخيار لا يمكن أن تكون فارغة", - "ONGOING_CALL": "مكالمة مستمرة", - "YOU_ALREADY_ONGOING_CALL": "أنت بالفعل في مكالمة مستمرة", - "RESIZE": "تغيير حجم", - "SETTINGS": "الإعدادات", - "ACTIONS": "الإجراءات", - "VIEW_PROFILE": "عرض الملف الشخصي", - "SEND_MESSAGE_IN_PRIVATE": "إرسال رسالة خاصة", - "DELETE": "حذف", - "DELETE_CONFIRM": "هل تريد بالتأكيد الحذف؟", - "CANCEL": "إلغاء", - "LEAVE_CONFIRM": "هل أنت متأكد أنك تريد مغادرة المجموعة؟", - "TRANSFER_CONFIRM": "أنت مالك المجموعة، يرجى نقل الملكية إلى عضو قبل مغادرة المجموعة", - "ADDING": "إضافة...", - "TRANSFER": "نقل", - "TRANSFERRING": "نقل", - "YES": "نعم", - "NO": "لا", - "SOMETHING_WRONG": "حدث خطأ ما، يرجى المحاولة مرة أخرى", - "INVALID_GROUP_NAME": "الرجاء إدخال اسم صالح للمجموعة والمحاولة مرة أخرى", - "INVALID_GROUP_TYPE": "الرجاء إدخال نوع صالح للمجموعة والمحاولة مرة أخرى", - "INVALID_PASSWORD": "الرجاء إدخال كلمة مرور صالحة للمجموعة والمحاولة مرة أخرى", - "WRONG_PASSWORD": "الرجاء إدخال كلمة المرور الصحيحة والمحاولة مرة أخرى", - "INVALID_POLL_QUESTION": "الرجاء إدخال السؤال المطلوب قبل إنشاء استطلاع للرأي", - "INVALID_POLL_OPTION": "الرجاء إدخال الإجابة المطلوبة قبل إنشاء استطلاع للرأي", - "SAME_LANGUAGE_MESSAGE": "لغة محددة للترجمة مشابهة للغة الرسالة الأصلية", - "LEAVE": "اترك", - "CALLS": "المكالمات", - "CUSTOM_MESSAGE_LOCATION": "📍 الموقع", - "OFFLINE": "غير متصل", - "YOU": "أنت", - "PRIVACY": "الخصوصية", - "BLOCKED_USERS": "المستخدمون المحظورون", - "YOU'VE_BLOCKED": "لقد حظرت", - "NO_PHOTOS": "لا توجد صور", - "NO_VIDEOS": "لا توجد فيديوهات", - "NO_DOCUMENTS": "لا توجد وثائق", - "JOIN": "انضم", - "DELETE_CONFIRM_MESSAGE": "هل ترغب في حذف هذه المحادثة؟ سيتم حذف هذه المحادثة من جميع أجهزتك.", - "CHAT_ERROR_MESSAGE": "لا يمكن تحميل الدردشات. يرجى المحاولة مرة أخرى", - "TRY_AGAIN": "حاول مرة أخرى", - "CONFIRM": "قم بتأكيد", - "UNSAFE_CONTENT": "محتوى غير آمن", - "UNSAFE_CONFIRMATION": "هل تريد بالتأكيد مشاهدة هذا المحتوى غير الآمن؟", - "AUDIO_FILE": "ملف صوتي", - "SHARED_FILE": "ملف مشترك", - "OPEN_DOCUMENT": "افتح المستند", - "OPEN_WHITEBOARD": "لوح أبيض مفتوح", - "PEOPLE_VOTED": "تصويت الناس", - "ADD_TO_CHAT": "إضافة إلى الدردشة", - "TAKE_PHOTO": "التقط صورة", - "SET_THE_ANSWERS": "قم بتعيين الإجابات", - "ADD_ANOTHER_ANSWER": "إضافة إجابة أخرى", - "ANSWER": "إجابة", - "IN_A_THREAD": "في موضوع ⤵", - "ERROR_GROUP_CREATE": "لا يمكن إنشاء مجموعة", - "TRY_AGAIN_LATER": "يرجى المحاولة مرة أخرى لاحقًا", - "CONTINUE": "استمر", - "GROUP_NAME_MAX": "يُسمح بـ 25 حرفًا كحد أقصى في اسم المجموعة", - "PASSWORD_MAX": "يسمح بحد أقصى 16 حرفاً في كلمة مرور المجموعة", - "GROUP_PASSWORD": "كلمة مرور المجموعة", - "INCORRECT_PASSWORD": "كلمة مرور غير صحيحة", - "OPEN_WHITEBOARD_TO_DRAW": "افتح السبورة للرسم معًا", - "CALL_HISTORY": "سجل المكالمات", - "NO_CALL_HISTORY": "لم يتم إجراء أي مكالمة حتى الآن", - "CALL_DETAILS": "تفاصيل المكالمة", - "CONFERENCE_CALL": "مكالمة جماعية", - "NEW_GROUP": "مجموعة جديدة", - "TRANSFER_OWNERSHIP": "نقل الملكية", - "COPY_MESSAGE": "نسخ", - "SHARE": "شارك", - "OPEN_DOCUMENT_TO_DRAW": "افتح المستند للرسم معًا", - "INFORMATION": "معلومات", - "COPY_TEXT": "نسخ النص", - "FORWARD": "إلى الأمام", - "TEXT": "النص", - "INCOMING_CALL": "مكالمة واردة", - "OUTGOING_CALL": "مكالمة صادرة", - "MISSED_CALL": "مكالمة فائتة", - "NEW_CONVERSATION": "دردشة جديدة", - "FORWARDING": "إرسال الرسائل..", - "READ": "اقرأ", - "No_RECIPIENT": "لا يوجد مستلم", - "RECEIPT_INFORMATION": "معلومات الاستلام", - "MESSAGE": "رسالة", - "GENERATING_SUMMARY":"ملخص التوليد", - "CONVERSATION_SUMMARY":"ملخص المحادثة", - "GENERATE_SUMMARY":"قم بإنشاء ملخص", - "COMETCHAT_BOT_FIRST_MESSAGE":"كيف يمكنني مساعدتك في هذه المحادثة؟ من فضلك اسألني سؤالاً وسأنصحك 🙂", - "COMETCHAT_ASK_BOT":"اسأل", - "COMETCHAT_ASK_AI_BOT":"اسأل روبوتات الذكاء الاصطناعي", - "COMETCHAT_ASK_BOT_SUBTITLE":"بوت الذكاء الاصطناعي", - "FORM_COMPLETION_MESSAGE": "شكرًا لك على ملء النموذج.", - "FORM":"نموذج", - "CARD":"بطاقة", - "RECORDINGS":"التسجيلات", - "PARTICIPANTS":"مشاركون", - "NO_PARTICIPANTS":"لا يوجد مشاركون", - "NO_RECORDINGS":"لا توجد تسجيلات", - "CALL_LOGS":"سجلات المكالمات", - "CANCELLED_AUDIO_CALL":"مكالمة صوتية ملغاة", - "CANCELLED_VIDEO_CALL":"مكالمة فيديو ملغاة", - "MICROPHONE_PERMISSION":"نحن نستخدم الميكروفون لتسجيل الرسائل الصوتية ومشاركتها. انتقل إلى الإعدادات لتمكين الوصول إلى الميكروفون" -} \ No newline at end of file + "USERS": "مستخدمون", + "CHATS": "الدردشات", + "GROUPS": "المجموعات", + "MORE": "المزيد", + "MESSAGE_IMAGE": "📷 صورة", + "MESSAGE_FILE": "📁 ملف", + "MESSAGE_VIDEO": "📹 فيديو", + "MESSAGE_AUDIO": "🎵 الصوت", + "CUSTOM_MESSAGE": "لديك رسالة", + "MISSED_VOICE_CALL": "مكالمة صوتية فائتة", + "MISSED_VIDEO_CALL": "مكالمة فيديو فائتة", + "CUSTOM_MESSAGE_POLL": "📊 استطلاع", + "CUSTOM_MESSAGE_STICKER": "💟 ملصق", + "CUSTOM_MESSAGE_DOCUMENT": "📃 مستند", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 سبورة بيضاء", + "ONLINE": "عبر الإنترنت", + "ADMINISTRATOR": "المسؤول", + "MODERATOR": "مقدم", + "PARTICIPANT": "مشارك", + "SUGGEST_A_REPLY": "اقترح الرد", + "GENERATIONG_ICEBREAKER": "توليد كاسحات الجليد", + "PUBLIC": "الجمهور", + "NO_REPLIES_FOUND": "لم يتم العثور على ردود", + "PRIVATE": "خاص", + "PASSWORD_PROTECTED": "محمي بكلمة مرور", + "PRIVACY_AND_SECURITY": "الخصوصية والأمان", + "PREFERENCES": "التفضيلات", + "MEMBERS": "الأعضاء", + "TODAY": "اليوم", + "YESTERDAY": "يوم أمس", + "SUNDAY": "الأحد", + "MONDAY": "الإثنين", + "TUESDAY": "الثلاثاء", + "WEDNESDAY": "الأربعاء", + "THURSDAY": "الخميس", + "FRIDAY": "الجمعة", + "SATURDAY": "يوم السبت", + "TYPING": "الكتابة...", + "IS_TYPING": "يقوم بالكتابة...", + "CLOSE": "أغلق", + "ENTER_GROUP_NAME": "أدخل اسم المجموعة", + "ADD_MEMBERS": "إضافة أعضاء", + "SEND_MESSAGE": "أرسل رسالة", + "UNBLOCK_USER": "إلغاء حظر المستخدم", + "BLOCK_USER": "حظر المستخدم", + "DELETE_AND_EXIT": "الحذف والخروج", + "LEAVE_GROUP": "اترك المجموعة", + "CREATE_GROUP": "إنشاء مجموعة", + "SHARED_MEDIA": "وسائل الإعلام المشتركة", + "VIDEO_CALL": "مكالمة فيديو", + "AUDIO_CALL": "مكالمة صوتية", + "LOADING": "جاري التحميل...", + "REPLY": "الرد", + "REPLIES": "الردود", + "LAUNCH": "إطلاق", + "SHARED_COLLABORATIVE_DOCUMENT": "شارك مستندًا تعاونيًا", + "SHARED_COLLABORATIVE_WHITEBOARD": "قام بمشاركة لوحة بيضاء تعاونية", + "CREATED_WHITEBOARD": "لقد قمت بإنشاء لوحة بيضاء تعاونية جديدة", + "CREATED_DOCUMENT": "لقد قمت بإنشاء مستند SomCollaborative جديد", + "PHOTOS": "صور", + "VIDEOS": "مقاطع فيديو", + "DOCUMENT": "مستند", + "MESSAGE_IS_DELETED": "تم حذف الرسالة", + "THIS_MESSAGE_DELETED": "⚠️ تم حذف هذه الرسالة", + "VIEW_ON_YOUTUBE": "شاهد على يوتيوب", + "SEARCH": "ابحث", + "NO_USERS_FOUND": "لم يتم العثور على أي مستخدم", + "ERROR": "خطأ", + "NO_GROUPS_FOUND": "لم يتم العثور على أي مجموعات", + "NO_CHATS_FOUND": "لم يتم العثور على محادثات", + "MEDIA_MESSAGE": "رسالة وسائل الإعلام", + "INCOMING_AUDIO_CALL": "مكالمة صوتية واردة", + "INCOMING_VIDEO_CALL": "مكالمة فيديو واردة", + "DECLINE": "تراجع", + "ACCEPT": "اقبل", + "CALL_INITIATED": "تم بدء المكالمة", + "OUTGOING_AUDIO_CALL": "مكالمة صوتية صادرة", + "OUTGOING_VIDEO_CALL": "مكالمة فيديو صادرة", + "CALL_REJECTED": "تم رفض المكالمة", + "REJECTED_CALL": "مكالمة مرفوضة", + "CALL_ACCEPTED": "تم قبول المكالمة", + "JOINED": "انضم", + "LEFT_THE_CALL": "تركت المكالمة", + "UNANSWERED_AUDIO_CALL": "مكالمة صوتية لم يتم الرد عليها", + "UNANSWERED_VIDEO_CALL": "مكالمة فيديو لم يتم الرد عليها", + "CALL_ENDED": "تم إنهاء المكالمة", + "CANCELLED_CALL": "مكالمة ملغاة", + "CALL_BUSY": "اتصل مشغول", + "CALLING": "جاري الاتصال...", + "ADD": "أضِف", + "NO_BANNED_MEMBERS_FOUND": "لم يتم العثور على أعضاء محظورين", + "BANNED_MEMBERS": "الأعضاء المحظورون", + "NAME": "الاسم", + "SCOPE": "النطاق", + "UNBAN": "إلغاء الحظر", + "SELECT_GROUP_TYPE": "حدد نوع المجموعة", + "ENTER_GROUP_PASSWORD": "أدخل كلمة مرور المجموعة", + "CREATE": "ابتكر", + "CREATE_POLL": "إنشاء استطلاع", + "QUESTION": "سؤال", + "ENTER_YOUR_QUESTION": "أدخل سؤالك", + "OPTIONS": "خيارات", + "ENTER_YOUR_OPTION": "أدخل الخيار الخاص بك", + "ADD_NEW_OPTION": "إضافة خيار جديد", + "VIEW_MEMBERS": "عرض الأعضاء", + "DETAILS": "التفاصيل", + "NOTIFICATIONS": "الإشعارات", + "OTHER": "أخرى", + "HELP": "مساعدة", + "REPORT_PROBLEM": "الإبلاغ عن مشكلة", + "GROUP_MEMBERS": "أعضاء المجموعة", + "BAN": "حظر", + "KICK": "ركلة", + "PICK_YOUR_EMOJI": "اختر الرمز التعبيري الخاص بك", + "PRIVATE_GROUP": "مجموعة خاصة", + "PROTECTED_GROUP": "مجموعة محمية", + "VISIT": "زيارة", + "ATTACH": "أرفق", + "ATTACH_FILE": "أرفق ملف", + "ATTACH_VIDEO": "أرفق مقطع فيديو", + "ATTACH_AUDIO": "أرفق الصوت", + "ATTACH_IMAGE": "أرفق صورة", + "COLLABORATIVE_DOCUMENT": "وثيقة تعاونية", + "COLLABORATIVE_WHITEBOARD": "السبورة التعاونية", + "COLLABORATE_USING_DOCUMENT": "التعاون باستخدام مستند", + "COLLABORATE_USING_WHITEBOARD": "التعاون باستخدام لوحة بيضاء", + "EMOJI": "رمز تعبيري", + "ENTER_YOUR_MESSAGE_HERE": "أدخل رسالتك هنا", + "NO_MESSAGES_FOUND": "لا توجد رسائل هنا حتى الآن...", + "THREAD": "خيط", + "ADD_REACTION": "إضافة رد فعل", + "NO_STICKERS_FOUND": "لم يتم العثور على ملصقات", + "REPLY_TO_THREAD": "الرد على الموضوع", + "REPLY_IN_THREAD": "الرد في الموضوع", + "DELETE_MESSAGE": "حذف رسالة", + "EDIT_MESSAGE": "تحرير رسالة", + "OWNER": "مالك", + "CHANGE_SCOPE": "تغيير النطاق", + "STICKER": "ملصق", + "LAST_ACTIVE_AT": "آخر عمل نشط", + "VOICE_CALL": "مكالمة صوتية", + "VIEW_DETAIL": "عرض التفاصيل", + "VOTES": "التصويت", + "VOTE": "تصويت", + "NO_VOTE": "لا يوجد تصويت", + "REACTED": "رد فعل", + "ADDED": "أضاف", + "UNBANNED": "غير محظور", + "MADE": "صنع", + "UNANSWERED_CALL": "مكالمة لم يتم الرد عليها", + "MISSED_AUDIO_CALL": "مكالمة صوتية فائتة", + "ENTER_YOUR_PASSWORD": "أدخل كلمة المرور الخاصة بك", + "DOCS": "مستندات", + "NO_RECORDS_FOUND": "لم يتم العثور على أية سجلات", + "LIVE_REACTION": "رد فعل مباشر", + "SMILEY_PEOPLE": "الابتسامات والأشخاص", + "ANIMALES_NATURE": "الحيوانات والطبيعة", + "FOOD_DRINK": "الطعام والشراب", + "ACTIVITY": "نشاط", + "TRAVEL_PLACES": "السفر والأماكن", + "OBJECTS": "الكائنات", + "SYMBOLS": "الرموز", + "FLAGS": "أعلام", + "SENT": "أُرسلت", + "SEEN": "شوهد", + "DELIVERED": "تم التوصيل", + "TRANSLATE_MESSAGE": "ترجمة رسالة", + "LEFT": "يسار", + "KICKED": "ركل", + "BANNED": "محظور", + "NEW_MESSAGES": "رسائل جديدة", + "NEW_MESSAGE": "رسالة جديدة", + "JUMP": "اقفز", + "SELECT_VIDEO_SOURCE": "حدد مصدر الفيديو", + "SELECT_INPUT_AUDIO_SOURCE": "حدد مصدر إدخال الصوت", + "SELECT_OUTPUT_AUDIO_SOURCE": "حدد مصدر إخراج الصوت", + "INITIATED_GROUP_CALL": "قام ببدء مكالمة جماعية", + "YOU_INITIATED_GROUP_CALL": "لقد بدأت مكالمة جماعية", + "IGNORE": "تجاهل", + "ON_ANOTHER_CALL": "في مكالمة أخرى", + "CREATING": "إنشاء", + "AVATAR": "أفاتار", + "ONGOING_CALL": "مكالمة مستمرة", + "YOU_ALREADY_ONGOING_CALL": "أنت بالفعل في مكالمة مستمرة", + "RESIZE": "تغيير الحجم", + "SETTINGS": "إعدادات", + "ACTIONS": "الإجراءات", + "VIEW_PROFILE": "عرض الملف الشخصي", + "SEND_MESSAGE_IN_PRIVATE": "أرسل رسالة بشكل خاص", + "DELETE": "حذف", + "DELETE_CONFIRM": "هل تريد بالتأكيد الحذف؟", + "CANCEL": "إلغاء", + "LEAVE_CONFIRM": "هل أنت متأكد أنك تريد مغادرة المجموعة؟", + "TRANSFER_CONFIRM": "أنت مالك المجموعة؛ يرجى نقل الملكية إلى أحد الأعضاء قبل مغادرة المجموعة", + "ADDING": "إضافة...", + "TRANSFER": "نقل", + "TRANSFERRING": "نقل", + "YES": "نعم", + "NO": "لا", + "SOMETHING_WRONG": "حدث خطأ ما؛ يرجى المحاولة مرة أخرى.", + "INVALID_GROUP_NAME": "الرجاء إدخال اسم صالح للمجموعة والمحاولة مرة أخرى", + "INVALID_PASSWORD": "يرجى إدخال كلمة مرور صالحة للمجموعة والمحاولة مرة أخرى", + "INVALID_GROUP_TYPE": "الرجاء إدخال نوع صالح للمجموعة والمحاولة مرة أخرى", + "WRONG_PASSWORD": "يرجى إدخال كلمة المرور الصحيحة والمحاولة مرة أخرى", + "INVALID_POLL_QUESTION": "الرجاء إدخال السؤال المطلوب قبل إنشاء استطلاع", + "INVALID_POLL_OPTION": "يرجى إدخال الإجابة المطلوبة قبل إنشاء استطلاع", + "SAME_LANGUAGE_MESSAGE": "اللغة المحددة للترجمة مشابهة للغة الرسالة الأصلية", + "LEAVE": "أجازة", + "CUSTOM_MESSAGE_LOCATION": "📍 الموقع", + "IN_A_THREAD": "في موضوع ⤵", + "CALLS": "مكالمات", + "OFFLINE": "غير متصل", + "YOU": "أنت", + "PRIVACY": "الخصوصية", + "BLOCKED_USERS": "المستخدمون المحظورون", + "YOU'VE_BLOCKED": "لقد حظرت", + "NO_PHOTOS": "لا توجد صور", + "NO_VIDEOS": "لا توجد مقاطع فيديو", + "NO_DOCUMENTS": "لا توجد مستندات", + "GENERATING_REPLIES": "توليد الردود", + "JOIN": "انضم", + "DELETE_CONFIRM_MESSAGE": "هل ترغب في حذف هذه المحادثة؟ سيتم حذف هذه المحادثة من جميع أجهزتك.", + "CHAT_ERROR_MESSAGE": "لا يمكن تحميل الدردشات. يرجى المحاولة مرة أخرى", + "TRY_AGAIN": "حاول مرة أخرى", + "CONFIRM": "قم بالتأكيد", + "UNSAFE_CONTENT": "محتوى غير آمن", + "UNSAFE_CONFIRMATION": "هل تريد بالتأكيد رؤية هذا المحتوى غير الآمن؟", + "AUDIO_FILE": "ملف صوتي", + "SHARED_FILE": "ملف مشترك", + "OPEN_DOCUMENT": "وثيقة مفتوحة", + "OPEN_WHITEBOARD": "لوح أبيض مفتوح", + "PEOPLE_VOTED": "صوت الناس", + "ADD_TO_CHAT": "إضافة إلى الدردشة", + "TAKE_PHOTO": "التقط صورة", + "SET_THE_ANSWERS": "قم بتعيين الإجابات", + "ADD_ANOTHER_ANSWER": "إضافة إجابة أخرى", + "ANSWER": "الإجابة", + "ERROR_GROUP_CREATE": "لا يمكن إنشاء مجموعة", + "TRY_AGAIN_LATER": "يرجى المحاولة مرة أخرى لاحقًا", + "CONTINUE": "استمر", + "GROUP_NAME_MAX": "يسمح بحد أقصى 25 حرفًا في اسم المجموعة", + "GROUP_PASSWORD_BLANK": "الرجاء إدخال كلمة المرور", + "PASSWORD_MAX": "الحد الأقصى المسموح به هو 16 حرفًا في كلمة مرور المجموعة", + "GROUP_PASSWORD": "كلمة مرور المجموعة", + "INCORRECT_PASSWORD": "كلمة مرور غير صحيحة", + "OPEN_WHITEBOARD_TO_DRAW": "افتح السبورة البيضاء للرسم معًا", + "CALL_HISTORY": "سجل المكالمات", + "NO_CALL_HISTORY": "لم يتم إجراء أي مكالمة حتى الآن", + "CALL_DETAILS": "تفاصيل المكالمة", + "CONFERENCE_CALL": "مكالمة جماعية", + "NEW_GROUP": "مجموعة جديدة", + "TRANSFER_OWNERSHIP": "نقل الملكية", + "COPY_MESSAGE": "نسخة", + "SHARE": "شارك", + "OPEN_DOCUMENT_TO_DRAW": "افتح المستند للرسم معًا", + "INFORMATION": "معلومات الرسالة", + "FORWARD": "إلى الأمام", + "COPY_TEXT": "نسخ النص", + "TEXT": "النص", + "INCOMING_CALL": "مكالمة واردة", + "OUTGOING_CALL": "مكالمة صادرة", + "MISSED_CALL": "مكالمة فائتة", + "GROUP_NAME_BLANK": "يجب ألا يكون اسم المجموعة فارغًا", + "GROUP_TYPE_BLANK": "نوع المجموعة لا يمكن أن تكون فارغة", + "POLL_QUESTION_BLANK": "علبة السؤال فارغة", + "POLL_OPTION_BLANK": "الخيار لا يمكن أن يكون فارغًا", + "NEW_CONVERSATION": "دردشة جديدة", + "FORWARDING": "جاري إرسال الرسائل...", + "READ": "اقرأ", + "No_RECIPIENT": "لا يوجد مستلم", + "RECEIPT_INFORMATION": "معلومات الاستلام", + "MESSAGE": "رسالة", + "GENERATING_SUMMARY": "ملخص التوليد", + "CONVERSATION_SUMMARY": "ملخص المحادثة", + "GENERATE_SUMMARY": "قم بإنشاء ملخص", + "COMETCHAT_BOT_FIRST_MESSAGE": "كيف يمكنني مساعدتك في هذه المحادثة؟ من فضلك اسألني سؤالاً وسأنصحك 🙂", + "COMETCHAT_ASK_BOT": "اسأل", + "COMETCHAT_ASK_AI_BOT": "اسأل روبوتات الذكاء الاصطناعي", + "COMETCHAT_ASK_BOT_SUBTITLE": "بوت الذكاء الاصطناعي", + "FORM_COMPLETION_MESSAGE": "شكرًا لك على ملء النموذج.", + "FORM": "نموذج", + "CARD": "بطاقة", + "RECORDINGS": "التسجيلات", + "PARTICIPANTS": "المشاركين", + "NO_PARTICIPANTS": "لا يوجد مشاركون", + "NO_RECORDINGS": "لا توجد تسجيلات", + "CALL_LOGS": "سجلات المكالمات", + "CANCELLED_AUDIO_CALL": "مكالمة صوتية ملغاة", + "CANCELLED_VIDEO_CALL": "مكالمة فيديو ملغاة", + "MEET_WITH": "اجتمع مع", + "MIN_MEETING": "الاجتماع الرئيسي", + "MORE_TIMES": "المزيد من المرات", + "SELECT_DAY": "حدد اليوم", + "SELECT_TIME": "حدد الوقت", + "NO_TIME_SLOT_AVAILABLE": "لا توجد فترة زمنية متاحة في هذا التاريخ. يرجى تجربة تاريخ آخر.", + "BOOK_NEW_SLOT": "احجز فتحة جديدة", + "TRY_AGAIN_CAMEL": "حاول مرة أخرى", + "TIME_SLOT_UNAVAILABLE": "لم تعد الفترة الزمنية متاحة. يرجى اختيار آخر.", + "MEETING_SCHEDULER": "جدولة الاجتماعات", + "MIN": "ميل" +} diff --git a/src/shared/resources/CometChatLocalize/resources/de/translation.json b/src/shared/resources/CometChatLocalize/resources/de/translation.json index 1be3d03..62c7d9e 100644 --- a/src/shared/resources/CometChatLocalize/resources/de/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/de/translation.json @@ -1,292 +1,302 @@ { - "USERS": "Nutzer", - "CHATS": "Chats", - "GROUPS": "Gruppen", - "MORE": "mehr", - "MESSAGE_IMAGE": "📷 Bild", - "MESSAGE_FILE": "📁 Datei", - "MESSAGE_VIDEO": "📹 Video", - "MESSAGE_AUDIO": "🎵 Audio", - "CUSTOM_MESSAGE": "Du hast eine Nachricht", - "MISSED_VOICE_CALL": "Sprachanruf verpasst", - "MISSED_VIDEO_CALL": "Videoanruf verpasst", - "CUSTOM_MESSAGE_POLL": "📊 Umfrage", - "CUSTOM_MESSAGE_STICKER": "💟 Aufkleber", - "CUSTOM_MESSAGE_DOCUMENT": "📃 Dokument", - "NO_REPLIES_FOUND":"Keine Antworten gefunden", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 Whiteboard", - "ONLINE": "Online", - "ADMINISTRATOR": "Verwalter", - "MODERATOR": "Moderator", - "GENERATING_REPLIES":"Antworten generieren", - "PARTICIPANT": "Teilnehmerin", - "PUBLIC": "Öffentlich", - "PRIVATE": "Privat", - "SUGGEST_A_REPLY":"Schlage eine Antwort vor", - "GENERATIONG_ICEBREAKER":"Eisbrecher erzeugen", - "PASSWORD_PROTECTED": "Passwort-geschützt", - "PRIVACY_AND_SECURITY": "Datenschutz und Sicherheit", - "PREFERENCES": "Präferenzen", - "MEMBERS": "Mitglieder", - "TODAY": "heute", - "YESTERDAY": "Gestern", - "SUNDAY": "Sonntag", - "MONDAY": "Montag", - "TUESDAY": "Dienstag", - "WEDNESDAY": "Mittwoch", - "THURSDAY": "Donnerstag", - "FRIDAY": "Freitag", - "SATURDAY": "samstag", - "TYPING": "tippen...", - "IS_TYPING": "tippt...", - "CLOSE": "schliessen", - "ENTER_GROUP_NAME": "Gruppennamen eingeben", - "ADD_MEMBERS": "Mitglieder hinzufügen", - "SEND_MESSAGE": "Nachricht senden", - "UNBLOCK_USER": "Benutzer entsperren", - "BLOCK_USER": "Benutzer blockieren", - "DELETE_AND_EXIT": "Löschen und beenden", - "LEAVE_GROUP": "Verlasse die Gruppe", - "CREATE_GROUP": "Gruppe erstellen", - "SHARED_MEDIA": "Geteilte Medien", - "VIDEO_CALL": "Videoanruf", - "AUDIO_CALL": "Audio-Anruf", - "LOADING": "Wird geladen...", - "REPLY": "Antwort", - "REPLIES": "Antworten", - "LAUNCH": "starten", - "SHARED_COLLABORATIVE_DOCUMENT": "hat ein gemeinschaftliches Dokument geteilt", - "SHARED_COLLABORATIVE_WHITEBOARD": "hat ein kollaboratives Whiteboard geteilt", - "CREATED_WHITEBOARD": "Du hast ein neues kollaboratives Whiteboard erstellt", - "CREATED_DOCUMENT": "Sie haben ein neues kollaboratives Dokument erstellt", - "PHOTOS": "Fotos", - "VIDEOS": "VIDEOS", - "DOCUMENT": "dokument", - "YOU_DELETED_THIS_MESSAGE": "⚠️ Du hast diese Nachricht gelöscht", - "THIS_MESSAGE_DELETED": "⚠️ Diese Nachricht wurde gelöscht", - "VIEW_ON_YOUTUBE": "Auf Youtube ansehen", - "SEARCH": "Suche", - "NO_USERS_FOUND": "Keine Benutzer gefunden", - "ERROR": "Fehler", - "NO_GROUPS_FOUND": "Keine Gruppen gefunden", - "NO_CHATS_FOUND": "Keine Chats gefunden", - "MEDIA_MESSAGE": "Mediale Botschaft", - "INCOMING_AUDIO_CALL": "Eingehender Audioanruf", - "INCOMING_VIDEO_CALL": "Eingehender Videoanruf", - "DECLINE": "Rückgang", - "ACCEPT": "Akzeptieren", - "CALL_INITIATED": "Anruf initiiert", - "OUTGOING_AUDIO_CALL": "Ausgehender Audioanruf", - "OUTGOING_VIDEO_CALL": "Ausgehender Videoanruf", - "CALL_REJECTED": "Anruf abgelehnt", - "REJECTED_CALL": "Anruf abgelehnt", - "CALL_ACCEPTED": "Anruf akzeptiert", - "JOINED": "verbunden", - "LEFT_THE_CALL": "hat den Anruf verlassen", - "UNANSWERED_AUDIO_CALL": "Unbeantworteter Audioan", - "UNANSWERED_VIDEO_CALL": "Unbeantworteter Videoan", - "CALL_ENDED": "Anruf endete", - "CANCELLED_CALL": "Anruf abgesagt", - "CALL_BUSY": "Anruf beschäftigt", - "CALLING": "Rufen...", - "ADD": "Add", - "NO_BANNED_MEMBERS_FOUND": "Keine verbotenen Mitglieder gefunden", - "BANNED_MEMBERS": "Verbotene Mitglieder", - "NAME": "Nennen", - "SCOPE": "Scope", - "UNBAN": "Unban", - "SELECT_GROUP_TYPE": "Gruppentyp wählen", - "ENTER_GROUP_PASSWORD": "Gruppenkennwort eingeben", - "CREATE": "Erstellen", - "CREATE_POLL": "Umfrage erstellen", - "QUESTION": "Frage", - "ENTER_YOUR_QUESTION": "Gib deine Frage ein", - "OPTIONS": "Optionen", - "ENTER_YOUR_OPTION": "Geben Sie Ihre Option", - "ADD_NEW_OPTION": "Neue Option hinzufügen", - "VIEW_MEMBERS": "Mitglieder ansehen", - "DETAILS": "Einzelheiten", - "NOTIFICATIONS": "Benachrichtigungen", - "OTHER": "andere", - "HELP": "Hilfe", - "REPORT_PROBLEM": "Melden Sie ein Problem", - "GROUP_MEMBERS": "Mitglieder der Gruppe", - "BAN": "Ban", - "KICK": "Tritt", - "PICK_YOUR_EMOJI": "Wähle dein Emoji", - "PRIVATE_GROUP": "Private Gruppe", - "PROTECTED_GROUP": "Geschützte Gruppe", - "VISIT": "Besuch", - "ATTACH": "anhängen", - "ATTACH_FILE": "Datei anhängen", - "ATTACH_VIDEO": "Video anhängen", - "ATTACH_AUDIO": "Anhängen von Audio", - "ATTACH_IMAGE": "Bild anhängen", - "COLLABORATE_USING_DOCUMENT": "Zusammenarbeit mit einem Dokument", - "COLLABORATE_USING_WHITEBOARD": "Arbeiten Sie mit einem Whiteboard zusammen", - "EMOJI": "Emoji", - "ENTER_YOUR_MESSAGE_HERE": "Gib hier deine Nachricht ein", - "NO_MESSAGES_FOUND": "Keine Nachrichten gefunden", - "THREAD": "Gewinde", - "COLLABORATIVE_DOCUMENT": "Gemeinschaftliches Dokument", - "COLLABORATIVE_WHITEBOARD": "Kollaboratives Whiteboard", - "ADD_REACTION": "Reaktion hinzufügen", - "NO_STICKERS_FOUND": "Keine Aufkleber gefunden", - "REPLY_TO_THREAD": "Antwort auf Thread", - "REPLY_IN_THREAD": "Antwort im Thread", - "DELETE_MESSAGE": "Nachricht löschen", - "EDIT_MESSAGE": "Nachricht bearbeiten", - "OWNER": "Inhaber", - "CHANGE_SCOPE": "Umfang ändern", - "STICKER": "aufkleber", - "LAST_ACTIVE_AT": "Zuletzt aktiv bei", - "VOICE_CALL": "Sprach-Anruf", - "VIEW_DETAIL": "Details anzeigen", - "VOTES": "Wahlen", - "VOTE": "Abstimmung", - "NO_VOTE": "Keine Abstimmung", - "REACTED": "reagiert", - "ADDED": "hinzugefügt", - "UNBANNED": "unverbannt", - "MADE": "hergestellt", - "UNANSWERED_CALL": "Unbeantworteter Anruf", - "MISSED_AUDIO_CALL": "Audioanruf verpasst", - "ENTER_YOUR_PASSWORD": "Geben Sie Ihr Passwort ein", - "DOCS": "docs", - "NO_RECORDS_FOUND": "Keine Aufzeichnungen gefunden", - "LIVE_REACTION": "Live-Reaktion", - "SMILEY_PEOPLE": "Smileys & Leute", - "ANIMALES_NATURE": "Tiere & Natur", - "FOOD_DRINK": "Essen & Trinken", - "ACTIVITY": "Die Aktivität", - "TRAVEL_PLACES": "Reisen & Orte", - "OBJECTS": "objekte", - "SYMBOLS": "Die Symbole", - "FLAGS": "Flaggen", - "SENT": "Gesendet", - "SEEN": "Gesehen", - "DELIVERED": "Ausgeliefert", - "TRANSLATE_MESSAGE": "Nachricht übersetzen", - "LEFT": "links", - "KICKED": "getreten", - "BANNED": "unerlaubt", - "NEW_MESSAGES": "neue Nachrichten", - "NEW_MESSAGE": "neue Nachricht", - "JUMP": "springen", - "SELECT_VIDEO_SOURCE": "Wählen Sie Videoquelle", - "SELECT_INPUT_AUDIO_SOURCE": "Wählen Sie die Audioquelle", - "SELECT_OUTPUT_AUDIO_SOURCE": "Wählen Sie die Ausgangs-Audioquelle", - "INITIATED_GROUP_CALL": "hat einen Gruppenanruf initiiert", - "YOU_INITIATED_GROUP_CALL": "Du hast einen Gruppenanruf initiiert", - "IGNORE": "ignorieren", - "ON_ANOTHER_CALL": "ist in einem anderen Anruf", - "CREATING": "Erstellen", - "AVATAR": "Avatar", - "GROUP_NAME_BLANK": "Gruppenname ist nicht leer", - "GROUP_TYPE_BLANK": "Gruppentyp ist nicht leer", - "GROUP_PASSWORD_BLANK": "Gruppenkennwort darf nicht leer sein", - "POLL_QUESTION_BLANK": "Frage wird nicht leer sein", - "POLL_OPTION_BLANK": "Option kannte nicht leer sein", - "ONGOING_CALL": "Laufender Aufruf", - "YOU_ALREADY_ONGOING_CALL": "Sie befinden sich bereits in einem laufenden Anruf", - "RESIZE": "Größe ändern", - "SETTINGS": "Einstellungen", - "ACTIONS": "Aktionen", - "VIEW_PROFILE": "Profil ansehen", - "SEND_MESSAGE_IN_PRIVATE": "Nachricht privat senden", - "DELETE": "löschen", - "DELETE_CONFIRM": "Bist du sicher, dass du löschen möchtest?", - "CANCEL": "Abbrechen", - "LEAVE_CONFIRM": "Bist du sicher, dass du die Gruppe verlassen willst?", - "TRANSFER_CONFIRM": "Sie sind der Gruppeninhaber, bitte übertragen Sie das Eigentum an ein Mitglied, bevor Sie die Gruppe verlassen", - "ADDING": "Hinzufügen...", - "TRANSFER": "Übertragen", - "TRANSFERRING": "Übertragen", - "YES": "ja", - "NO": "Nein", - "SOMETHING_WRONG": "Etwas ist schief gelaufen, bitte versuche es noch einmal", - "INVALID_GROUP_NAME": "Bitte geben Sie einen gültigen Namen für die Gruppe ein und versuchen Sie es erneut", - "INVALID_GROUP_TYPE": "Bitte geben Sie einen gültigen Typ für die Gruppe ein und versuchen Sie es erneut", - "INVALID_PASSWORD": "Bitte geben Sie ein gültiges Passwort für die Gruppe ein und versuchen Sie es erneut", - "WRONG_PASSWORD": "Bitte geben Sie das richtige Passwort ein und versuchen Sie es erneut", - "INVALID_POLL_QUESTION": "Bitte geben Sie die gewünschte Frage ein, bevor Sie eine Umfrage erstellen", - "INVALID_POLL_OPTION": "Bitte geben Sie die erforderliche Antwort ein, bevor Sie eine Umfrage erstellen", - "SAME_LANGUAGE_MESSAGE": "Die ausgewählte Sprache für die Übersetzung ähnelt der Sprache der Originalnachricht", - "LEAVE": "Verlassen", - "CALLS": "Anrufe", - "CUSTOM_MESSAGE_LOCATION": "📍 Standort", - "OFFLINE": "Offline", - "YOU": "Du", - "PRIVACY": "Datenschutz", - "BLOCKED_USERS": "Gesperrte", - "YOU'VE_BLOCKED": "Du hast geblockt", - "NO_PHOTOS": "Keine Fotos", - "NO_VIDEOS": "Keine Videos", - "NO_DOCUMENTS": "Keine Dokumente", - "JOIN": "Beitreten", - "DELETE_CONFIRM_MESSAGE": "Möchten Sie diese Konversation löschen? Diese Konversation wird von all Ihren Geräten gelöscht.", - "CHAT_ERROR_MESSAGE": "Chats können nicht geladen werden. Bitte versuche es erneut", - "TRY_AGAIN": "VERSUCHE ES NOCH EINMAL", - "CONFIRM": "Bestätigen", - "UNSAFE_CONTENT": "UNSICHERER INHALT", - "UNSAFE_CONFIRMATION": "MÖCHTEN SIE DIESEN UNSICHEREN INHALT SICHER SEHEN?", - "AUDIO_FILE": "AUDIODATEI", - "SHARED_FILE": "GETEILTE DATEI", - "OPEN_DOCUMENT": "DOKUMENT ÖFFNEN", - "OPEN_WHITEBOARD": "WHITEBOARD ÖFFNEN", - "PEOPLE_VOTED": "LEUTE WÄHLEN", - "ADD_TO_CHAT": "ZU CHA HINZUFÜGEN", - "TAKE_PHOTO": "MACH EIN FOTO", - "SET_THE_ANSWERS": "STELLE DIE ANTWORTEN EIN", - "ADD_ANOTHER_ANSWER": "EINE WEITERE ANTWORT HINZUFÜGEN", - "ANSWER": "ANTWORT", - "IN_A_THREAD": "In einem Thread ⤵", - "ERROR_GROUP_CREATE": "Gruppe kann nicht erstellt werden", - "TRY_AGAIN_LATER": "Bitte versuchen Sie es später erneut", - "CONTINUE": "Weiter", - "GROUP_NAME_MAX": "Im Gruppennamen sind maximal 25 Zeichen zulässig", - "PASSWORD_MAX": "Maximal 16 Zeichen sind im Gruppenpasswort zulässig", - "GROUP_PASSWORD": "Gruppen-Passwort", - "INCORRECT_PASSWORD": "Falsches Passwort", - "OPEN_WHITEBOARD_TO_DRAW": "Whiteboard öffnen, um gemeinsam zu zeichnen", - "CALL_HISTORY": "Anrufliste", - "NO_CALL_HISTORY": "Es wurde noch kein Anruf getätigt", - "CALL_DETAILS": "Einzelheiten des Anrufs", - "CONFERENCE_CALL": "Telefonkonferenz", - "NEW_GROUP": "Neue Gruppe", - "TRANSFER_OWNERSHIP": "Eigentum übertragen", - "COPY_MESSAGE": "Kopieren", - "SHARE": "Teilen", - "OPEN_DOCUMENT_TO_DRAW": "Dokument öffnen, um es zusammen zu zeichnen", - "INFORMATION": "Informationen", - "COPY_TEXT": "Text kopieren", - "FORWARD": "Vorwärts", - "TEXT": "Text", - "INCOMING_CALL": "Eingehender Anruf", - "OUTGOING_CALL": "Ausgehender Anruf", - "MISSED_CALL": "Verpasster Anruf", - "NEW_CONVERSATION": "Neuer Chat", - "FORWARDING": "Nachrichten werden gesendet..", - "READ": "Lesen", - "No_RECIPIENT": "Kein Empfänger", - "RECEIPT_INFORMATION": "Informationen zum Zahlungseingang", - "MESSAGE": "Nachricht", - "GENERATING_SUMMARY":"Zusammenfassung wird generiert", - "CONVERSATION_SUMMARY":"Zusammenfassung der Konversation", - "GENERATE_SUMMARY":"Generieren Sie eine Zusammenfassung", - "COMETCHAT_BOT_FIRST_MESSAGE":"Wie kann ich dir bei diesem Gespräch helfen? Bitte stell mir eine Frage und ich berate dich 🙂", - "COMETCHAT_ASK_BOT":"Frag", - "COMETCHAT_ASK_AI_BOT":"Fragen Sie KI-Bots", - "COMETCHAT_ASK_BOT_SUBTITLE":"KI-Bot", - "FORM_COMPLETION_MESSAGE": "Vielen Dank für das Ausfüllen des Formulars.", - "FORM":"Formular", - "CARD":"Karte", - "RECORDINGS":"Aufzeichnungen", - "PARTICIPANTS":"Teilnehmer", - "NO_PARTICIPANTS":"Keine Teilnehmer", - "NO_RECORDINGS":"Keine Aufzeichnungen", - "CALL_LOGS":"Anruflisten", - "CANCELLED_AUDIO_CALL":"Abgebrochener Audioanruf", - "CANCELLED_VIDEO_CALL":"Videoanruf abgesagt", - "MICROPHONE_PERMISSION":"Wir verwenden ein Mikrofon, um Audionachrichten aufzunehmen und zu teilen. Gehen Sie zu den Einstellungen, um den Mikrofonzugriff zu aktivieren" -} \ No newline at end of file + "USERS": "Nutzer", + "CHATS": "Chats", + "GROUPS": "Gruppen", + "MORE": "Mehr", + "MESSAGE_IMAGE": "📷 Bild", + "MESSAGE_FILE": "📁 Datei", + "MESSAGE_VIDEO": "📹 Video", + "MESSAGE_AUDIO": "🎵 Audio", + "CUSTOM_MESSAGE": "Du hast eine Nachricht", + "MISSED_VOICE_CALL": "Verpasster Sprachanruf", + "MISSED_VIDEO_CALL": "Videoanruf verpasst", + "CUSTOM_MESSAGE_POLL": "📊 Umfrage", + "CUSTOM_MESSAGE_STICKER": "💟 Aufkleber", + "CUSTOM_MESSAGE_DOCUMENT": "📃 Dokument", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 Whiteboard", + "ONLINE": "Online", + "ADMINISTRATOR": "Administrator", + "MODERATOR": "Moderator", + "PARTICIPANT": "Teilnehmer", + "SUGGEST_A_REPLY": "Schlage eine Antwort vor", + "GENERATIONG_ICEBREAKER": "Eisbrecher generieren", + "PUBLIC": "Öffentlich", + "NO_REPLIES_FOUND": "Keine Antworten gefunden", + "PRIVATE": "Privat", + "PASSWORD_PROTECTED": "Passwortgeschützt", + "PRIVACY_AND_SECURITY": "Datenschutz und Sicherheit", + "PREFERENCES": "Präferenzen", + "MEMBERS": "Mitglieder", + "TODAY": "Heute", + "YESTERDAY": "Gestern", + "SUNDAY": "Sonntag", + "MONDAY": "Montag", + "TUESDAY": "Dienstag", + "WEDNESDAY": "Mittwoch", + "THURSDAY": "Donnerstag", + "FRIDAY": "Freitag", + "SATURDAY": "Samstag", + "TYPING": "tippen...", + "IS_TYPING": "tippt...", + "CLOSE": "Schliessen", + "ENTER_GROUP_NAME": "Geben Sie den Gruppennamen ein", + "ADD_MEMBERS": "Mitglieder hinzufügen", + "SEND_MESSAGE": "Nachricht senden", + "UNBLOCK_USER": "Benutzer entsperren", + "BLOCK_USER": "Benutzer blockieren", + "DELETE_AND_EXIT": "Löschen und beenden", + "LEAVE_GROUP": "Gruppe verlassen", + "CREATE_GROUP": "Gruppe erstellen", + "SHARED_MEDIA": "Geteilte Medien", + "VIDEO_CALL": "Videoanruf", + "AUDIO_CALL": "Audioanruf", + "LOADING": "Wird geladen...", + "REPLY": "Antwort", + "REPLIES": "Antworten", + "LAUNCH": "Starten", + "SHARED_COLLABORATIVE_DOCUMENT": "hat ein kollaboratives Dokument geteilt", + "SHARED_COLLABORATIVE_WHITEBOARD": "hat ein kollaboratives Whiteboard geteilt", + "CREATED_WHITEBOARD": "Sie haben ein neues kollaboratives Whiteboard erstellt", + "CREATED_DOCUMENT": "Sie haben ein neues SOMCollaborative-Dokument erstellt", + "PHOTOS": "Fotos", + "VIDEOS": "Videos", + "DOCUMENT": "Dokument", + "MESSAGE_IS_DELETED": "Nachricht wird gelöscht", + "THIS_MESSAGE_DELETED": "⚠️ Diese Nachricht wurde gelöscht", + "VIEW_ON_YOUTUBE": "Auf Youtube ansehen", + "SEARCH": "Suche", + "NO_USERS_FOUND": "Keine Benutzer gefunden", + "ERROR": "Fehler", + "NO_GROUPS_FOUND": "Keine Gruppen gefunden", + "NO_CHATS_FOUND": "Keine Chats gefunden", + "MEDIA_MESSAGE": "Botschaft der Medien", + "INCOMING_AUDIO_CALL": "Eingehender Audioanruf", + "INCOMING_VIDEO_CALL": "Eingehender Videoanruf", + "DECLINE": "Rückgang", + "ACCEPT": "Akzeptieren", + "CALL_INITIATED": "Anruf initiiert", + "OUTGOING_AUDIO_CALL": "Ausgehender Audioanruf", + "OUTGOING_VIDEO_CALL": "Ausgehender Videoanruf", + "CALL_REJECTED": "Anruf abgelehnt", + "REJECTED_CALL": "Abgelehnter Anruf", + "CALL_ACCEPTED": "Anruf angenommen", + "JOINED": "verbunden", + "LEFT_THE_CALL": "hat den Anruf verlassen", + "UNANSWERED_AUDIO_CALL": "Unbeantworteter Audioanruf", + "UNANSWERED_VIDEO_CALL": "Unbeantworteter Videoanruf", + "CALL_ENDED": "Anruf wurde beendet", + "CANCELLED_CALL": "Anruf storniert", + "CALL_BUSY": "Ruf beschäftigt an", + "CALLING": "Ich rufe an...", + "ADD": "Hinzufügen", + "NO_BANNED_MEMBERS_FOUND": "Keine gesperrten Mitglieder gefunden", + "BANNED_MEMBERS": "Gesperrte Mitglieder", + "NAME": "Name", + "SCOPE": "Geltungsbereich", + "UNBAN": "Sperre aufheben", + "SELECT_GROUP_TYPE": "Wählen Sie den Gruppentyp", + "ENTER_GROUP_PASSWORD": "Geben Sie das Gruppenpasswort ein", + "CREATE": "Erstellen", + "CREATE_POLL": "Umfrage erstellen", + "QUESTION": "Frage", + "ENTER_YOUR_QUESTION": "Gib deine Frage ein", + "OPTIONS": "Optionen", + "ENTER_YOUR_OPTION": "Geben Sie Ihre Option ein", + "ADD_NEW_OPTION": "Neue Option hinzufügen", + "VIEW_MEMBERS": "Mitglieder ansehen", + "DETAILS": "Einzelheiten", + "NOTIFICATIONS": "Benachrichtigungen", + "OTHER": "Andere", + "HELP": "Hilfe", + "REPORT_PROBLEM": "Ein Problem melden", + "GROUP_MEMBERS": "Mitglieder der Gruppe", + "BAN": "Bann", + "KICK": "Tritt", + "PICK_YOUR_EMOJI": "Wähle dein Emoji", + "PRIVATE_GROUP": "Private Gruppe", + "PROTECTED_GROUP": "Geschützte Gruppe", + "VISIT": "Besuch", + "ATTACH": "Anhängen", + "ATTACH_FILE": "Datei anhängen", + "ATTACH_VIDEO": "Video anhängen", + "ATTACH_AUDIO": "Audio anhängen", + "ATTACH_IMAGE": "Bild anhängen", + "COLLABORATIVE_DOCUMENT": "Kollaboratives Dokument", + "COLLABORATIVE_WHITEBOARD": "Kollaboratives Whiteboard", + "COLLABORATE_USING_DOCUMENT": "Mithilfe eines Dokuments zusammenarbeiten", + "COLLABORATE_USING_WHITEBOARD": "Arbeiten Sie mit einem Whiteboard zusammen", + "EMOJI": "Emoji", + "ENTER_YOUR_MESSAGE_HERE": "Tragen Sie hier Ihre Nachricht ein", + "NO_MESSAGES_FOUND": "Noch keine Nachrichten hier...", + "THREAD": "Gewinde", + "ADD_REACTION": "Reaktion hinzufügen", + "NO_STICKERS_FOUND": "Keine Sticker gefunden", + "REPLY_TO_THREAD": "Auf Thread antworten", + "REPLY_IN_THREAD": "Im Thread antworten", + "DELETE_MESSAGE": "Nachricht löschen", + "EDIT_MESSAGE": "Nachricht bearbeiten", + "OWNER": "Besitzer", + "CHANGE_SCOPE": "Umfang ändern", + "STICKER": "Aufkleber", + "LAST_ACTIVE_AT": "Zuletzt aktiv am", + "VOICE_CALL": "Sprachanruf", + "VIEW_DETAIL": "Detail ansehen", + "VOTES": "Wahlen", + "VOTE": "Abstimmung", + "NO_VOTE": "Keine Abstimmung", + "REACTED": "reagiert", + "ADDED": "hinzugefügt", + "UNBANNED": "nicht gesperrt", + "MADE": "hergestellt", + "UNANSWERED_CALL": "Unbeantworteter Anruf", + "MISSED_AUDIO_CALL": "Verpasster Audioanruf", + "ENTER_YOUR_PASSWORD": "Gib dein Passwort ein", + "DOCS": "Dokumente", + "NO_RECORDS_FOUND": "Keine Datensätze gefunden", + "LIVE_REACTION": "Live-Reaktion", + "SMILEY_PEOPLE": "Smileys und Leute", + "ANIMALES_NATURE": "Tiere und Natur", + "FOOD_DRINK": "Essen & Trinken", + "ACTIVITY": "Aktivität", + "TRAVEL_PLACES": "Reisen und Orte", + "OBJECTS": "Objekte", + "SYMBOLS": "Symbole", + "FLAGS": "Flaggen", + "SENT": "Gesendet", + "SEEN": "Gesehen", + "DELIVERED": "Geliefert", + "TRANSLATE_MESSAGE": "Nachricht übersetzen", + "LEFT": "links", + "KICKED": "getreten", + "BANNED": "unerlaubt", + "NEW_MESSAGES": "neue Nachrichten", + "NEW_MESSAGE": "neue Nachricht", + "JUMP": "Springen", + "SELECT_VIDEO_SOURCE": "Wählen Sie die Videoquelle", + "SELECT_INPUT_AUDIO_SOURCE": "Wählen Sie die Eingangs-Audioquelle", + "SELECT_OUTPUT_AUDIO_SOURCE": "Wählen Sie die Ausgangs-Audioquelle", + "INITIATED_GROUP_CALL": "hat einen Gruppenanruf initiiert", + "YOU_INITIATED_GROUP_CALL": "Sie haben einen Gruppenanruf initiiert", + "IGNORE": "Ignorieren", + "ON_ANOTHER_CALL": "ist in einem anderen Gespräch", + "CREATING": "Erstellen", + "AVATAR": "Avatar", + "ONGOING_CALL": "Laufender Anruf", + "YOU_ALREADY_ONGOING_CALL": "Sie befinden sich bereits in einem laufenden Gespräch", + "RESIZE": "Größe ändern", + "SETTINGS": "Einstellungen", + "ACTIONS": "Aktionen", + "VIEW_PROFILE": "Profil ansehen", + "SEND_MESSAGE_IN_PRIVATE": "Nachricht privat senden", + "DELETE": "Löschen", + "DELETE_CONFIRM": "Bist du sicher, dass du löschen möchtest?", + "CANCEL": "Stornieren", + "LEAVE_CONFIRM": "Bist du sicher, dass du die Gruppe verlassen willst?", + "TRANSFER_CONFIRM": "Du bist der Gruppenbesitzer; bitte übertrage das Eigentum an ein Mitglied, bevor du die Gruppe verlässt", + "ADDING": "Wird hinzugefügt...", + "TRANSFER": "Übertragung", + "TRANSFERRING": "Übertragung", + "YES": "Ja", + "NO": "Nein", + "SOMETHING_WRONG": "Etwas ist schief gelaufen; Bitte versuchen Sie es erneut.", + "INVALID_GROUP_NAME": "Bitte geben Sie einen gültigen Namen für die Gruppe ein und versuchen Sie es erneut", + "INVALID_PASSWORD": "Bitte geben Sie ein gültiges Passwort für die Gruppe ein und versuchen Sie es erneut", + "INVALID_GROUP_TYPE": "Bitte geben Sie einen gültigen Typ für die Gruppe ein und versuchen Sie es erneut", + "WRONG_PASSWORD": "Bitte geben Sie das richtige Passwort ein und versuchen Sie es erneut", + "INVALID_POLL_QUESTION": "Bitte geben Sie die erforderliche Frage ein, bevor Sie eine Umfrage erstellen", + "INVALID_POLL_OPTION": "Bitte geben Sie die erforderliche Antwort ein, bevor Sie eine Umfrage erstellen", + "SAME_LANGUAGE_MESSAGE": "Die gewählte Sprache für die Übersetzung ähnelt der Sprache der Originalnachricht", + "LEAVE": "Verlassen", + "CUSTOM_MESSAGE_LOCATION": "📍 Standort", + "IN_A_THREAD": "In einem Thread ⤵", + "CALLS": "Anrufe", + "OFFLINE": "Offline", + "YOU": "Du", + "PRIVACY": "Datenschutz", + "BLOCKED_USERS": "Gesperrte Nutzer", + "YOU'VE_BLOCKED": "Du hast geblockt", + "NO_PHOTOS": "Keine Fotos", + "NO_VIDEOS": "Keine Videos", + "NO_DOCUMENTS": "Keine Dokumente", + "GENERATING_REPLIES": "Antworten generieren", + "JOIN": "Beitreten", + "DELETE_CONFIRM_MESSAGE": "Möchten Sie diese Konversation löschen? Diese Konversation wird von all Ihren Geräten gelöscht.", + "CHAT_ERROR_MESSAGE": "Chats können nicht geladen werden. Bitte versuche es erneut", + "TRY_AGAIN": "VERSUCHE ES NOCHMAL", + "CONFIRM": "Bestätigen", + "UNSAFE_CONTENT": "Unsicherer Inhalt", + "UNSAFE_CONFIRMATION": "Möchten Sie diesen unsicheren Inhalt sicherlich sehen?", + "AUDIO_FILE": "Audiodatei", + "SHARED_FILE": "Geteilte Datei", + "OPEN_DOCUMENT": "DOKUMENT ÖFFNEN", + "OPEN_WHITEBOARD": "WHITEBOARD ÖFFNEN", + "PEOPLE_VOTED": "Leute haben gewählt", + "ADD_TO_CHAT": "Zum Chat hinzufügen", + "TAKE_PHOTO": "Mach ein Foto", + "SET_THE_ANSWERS": "STELLE DIE ANTWORTEN EIN", + "ADD_ANOTHER_ANSWER": "Eine weitere Antwort hinzufügen", + "ANSWER": "Antwort", + "ERROR_GROUP_CREATE": "Gruppe kann nicht erstellt werden", + "TRY_AGAIN_LATER": "Bitte versuchen Sie es später erneut", + "CONTINUE": "Weiter", + "GROUP_NAME_MAX": "Maximal 25 Zeichen sind im Gruppennamen zulässig", + "GROUP_PASSWORD_BLANK": "Bitte geben Sie ein Passwort ein", + "PASSWORD_MAX": "Maximal 16 Zeichen sind im Gruppenpasswort zulässig", + "GROUP_PASSWORD": "Gruppen-Passwort", + "INCORRECT_PASSWORD": "Falsches Passwort", + "OPEN_WHITEBOARD_TO_DRAW": "Whiteboard öffnen, um gemeinsam zu zeichnen", + "CALL_HISTORY": "Anrufliste", + "NO_CALL_HISTORY": "Es wurde noch kein Anruf getätigt", + "CALL_DETAILS": "Einzelheiten zum Anruf", + "CONFERENCE_CALL": "Telefonkonferenz", + "NEW_GROUP": "Neue Gruppe", + "TRANSFER_OWNERSHIP": "Inhaberschaft übertragen", + "COPY_MESSAGE": "Kopieren", + "SHARE": "Teilen", + "OPEN_DOCUMENT_TO_DRAW": "Dokument zum Zusammenzeichnen öffnen", + "INFORMATION": "Informationen zur Nachricht", + "FORWARD": "Vorwärts", + "COPY_TEXT": "Text kopieren", + "TEXT": "Text", + "INCOMING_CALL": "Eingehender Anruf", + "OUTGOING_CALL": "Ausgehender Anruf", + "MISSED_CALL": "Verpasster Anruf", + "GROUP_NAME_BLANK": "Gruppenname sollte nicht leer sein", + "GROUP_TYPE_BLANK": "Der Gruppentyp „Kann nicht“ ist leer", + "POLL_QUESTION_BLANK": "Frage: canaut ist leer", + "POLL_OPTION_BLANK": "Option Kann nicht ist leer", + "NEW_CONVERSATION": "Neuer Chat", + "FORWARDING": "Nachrichten werden gesendet...", + "READ": "Lesen", + "No_RECIPIENT": "Kein Empfänger", + "RECEIPT_INFORMATION": "Informationen zur Quittung", + "MESSAGE": "Nachricht", + "GENERATING_SUMMARY": "Zusammenfassung wird generiert", + "CONVERSATION_SUMMARY": "Zusammenfassung der Konversation", + "GENERATE_SUMMARY": "Generieren Sie eine Zusammenfassung", + "COMETCHAT_BOT_FIRST_MESSAGE": "Wie kann ich dir bei diesem Gespräch helfen? Bitte stell mir eine Frage und ich berate dich 🙂", + "COMETCHAT_ASK_BOT": "Frag", + "COMETCHAT_ASK_AI_BOT": "Fragen Sie KI-Bots", + "COMETCHAT_ASK_BOT_SUBTITLE": "KI-Bot", + "FORM_COMPLETION_MESSAGE": "Vielen Dank für das Ausfüllen des Formulars.", + "FORM": "Formular", + "CARD": "Karte", + "RECORDINGS": "Aufzeichnungen", + "PARTICIPANTS": "Teilnehmer", + "NO_PARTICIPANTS": "Keine Teilnehmer", + "NO_RECORDINGS": "Keine Aufzeichnungen", + "CALL_LOGS": "Anrufprotokolle", + "CANCELLED_AUDIO_CALL": "Abgebrochener Audioanruf", + "CANCELLED_VIDEO_CALL": "Videoanruf abgesagt", + "MEET_WITH": "Treffen Sie sich mit", + "MIN_MEETING": "Hauptbesprechung", + "MORE_TIMES": "Mehrmals", + "SELECT_DAY": "Wählen Sie einen Tag", + "SELECT_TIME": "Wählen Sie eine Uhrzeit", + "NO_TIME_SLOT_AVAILABLE": "An diesem Tag ist kein Zeitfenster verfügbar. Bitte versuchen Sie es mit einem anderen Datum.", + "BOOK_NEW_SLOT": "Neuen Slot buchen", + "TRY_AGAIN_CAMEL": "Versuche es noch einmal", + "TIME_SLOT_UNAVAILABLE": "Das Zeitfenster ist nicht mehr verfügbar. Bitte wählen Sie ein anderes.", + "MEETING_SCHEDULER": "Planer für Besprechungen", + "MIN": "mi" +} diff --git a/src/shared/resources/CometChatLocalize/resources/en/translation.json b/src/shared/resources/CometChatLocalize/resources/en/translation.json index 92f8262..8a2c454 100644 --- a/src/shared/resources/CometChatLocalize/resources/en/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/en/translation.json @@ -18,10 +18,10 @@ "ADMINISTRATOR": "Administrator", "MODERATOR": "Moderator", "PARTICIPANT": "Participant", - "SUGGEST_A_REPLY" : "Suggest a reply", - "GENERATIONG_ICEBREAKER" : "Generating icebreakers", + "SUGGEST_A_REPLY": "Suggest a reply", + "GENERATIONG_ICEBREAKER": "Generating icebreakers", "PUBLIC": "Public", - "NO_REPLIES_FOUND":"No Replies Found", + "NO_REPLIES_FOUND": "No Replies Found", "PRIVATE": "Private", "PASSWORD_PROTECTED": "Password Protected", "PRIVACY_AND_SECURITY": "Privacy and Security", @@ -198,7 +198,7 @@ "TRANSFERRING": "Transferring", "YES": "Yes", "NO": "No", - "SOMETHING_WRONG": "Something went wrong, please try again", + "SOMETHING_WRONG": "Something went wrong, Please try again.", "INVALID_GROUP_NAME": "Please enter a valid name for the group and try again", "INVALID_PASSWORD": "Please enter a valid password for the group and try again", "INVALID_GROUP_TYPE": "Please enter a valid type for the group and try again", @@ -218,7 +218,7 @@ "NO_PHOTOS": "No Photos", "NO_VIDEOS": "No Videos", "NO_DOCUMENTS": "No Documents", - "GENERATING_REPLIES":"Generating replies", + "GENERATING_REPLIES": "Generating replies", "JOIN": "Join", "DELETE_CONFIRM_MESSAGE": "Would you like to delete this conversation? This conversation will be deleted from all of your devices.", "CHAT_ERROR_MESSAGE": "Can't load chats. Please try again", @@ -271,22 +271,32 @@ "No_RECIPIENT": "No Recipient", "RECEIPT_INFORMATION": "Receipt Information", "MESSAGE": "Message", - "GENERATING_SUMMARY": "Generating Summary", - "CONVERSATION_SUMMARY": "Conversation Summary", - "GENERATE_SUMMARY": "Generate a summary", + "GENERATING_SUMMARY": "Generating Summary", + "CONVERSATION_SUMMARY": "Conversation Summary", + "GENERATE_SUMMARY": "Generate a summary", "COMETCHAT_BOT_FIRST_MESSAGE": "How can I help you with this conversation? Please ask me a question and I’ll advice you 🙂", "COMETCHAT_ASK_BOT": "Ask", "COMETCHAT_ASK_AI_BOT": "Ask AI Bots", "COMETCHAT_ASK_BOT_SUBTITLE": "AI Bot", "FORM_COMPLETION_MESSAGE": "Thank you for filling out the form.", - "FORM":"Form", - "CARD":"Card", - "RECORDINGS":"Recordings", - "PARTICIPANTS":"Participants", - "NO_PARTICIPANTS":"No Participants", - "NO_RECORDINGS":"No Recordings", - "CALL_LOGS":"Call Logs", - "CANCELLED_AUDIO_CALL":"Cancelled audio call", - "CANCELLED_VIDEO_CALL":"Cancelled video call", - "MICROPHONE_PERMISSION": "We use microphone to record and share audio messages. Go to settings to enable microphone access" -} \ No newline at end of file + "FORM": "Form", + "CARD": "Card", + "RECORDINGS": "Recordings", + "PARTICIPANTS": "Participants", + "NO_PARTICIPANTS": "No Participants", + "NO_RECORDINGS": "No Recordings", + "CALL_LOGS": "Call Logs", + "CANCELLED_AUDIO_CALL": "Cancelled audio call", + "CANCELLED_VIDEO_CALL": "Cancelled video call", + "MEET_WITH": "Meet with", + "MIN_MEETING": "min meeting", + "MORE_TIMES": "More times", + "SELECT_DAY": "Select a Day", + "SELECT_TIME": "Select a Time", + "NO_TIME_SLOT_AVAILABLE": "No time slot available on this date. Please try another date.", + "BOOK_NEW_SLOT": "Book new slot", + "TRY_AGAIN_CAMEL": "Try again", + "TIME_SLOT_UNAVAILABLE": "Time slot no longer available. Please choose another.", + "MEETING_SCHEDULER": "Meeting Scheduler", + "MIN": "min" +} diff --git a/src/shared/resources/CometChatLocalize/resources/es/translation.json b/src/shared/resources/CometChatLocalize/resources/es/translation.json index 1f835c7..e4792d6 100644 --- a/src/shared/resources/CometChatLocalize/resources/es/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/es/translation.json @@ -1,292 +1,302 @@ { - "USERS": "Usuarios", - "CHATS": "Chats", - "GROUPS": "Grupos", - "MORE": "Más", - "MESSAGE_IMAGE": "📷 Imagen", - "MESSAGE_FILE": "📁 Archivo", - "MESSAGE_VIDEO": "📹 Vídeo", - "MESSAGE_AUDIO": "🎵 Audio", - "CUSTOM_MESSAGE": "Tienes un mensaje", - "MISSED_VOICE_CALL": "Llamada de voz perdida", - "MISSED_VIDEO_CALL": "Videollamada perdida", - "CUSTOM_MESSAGE_POLL": "📊 Encuesta", - "CUSTOM_MESSAGE_STICKER": "💟 Pegatina", - "CUSTOM_MESSAGE_DOCUMENT": "📃 Documento", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 Pizarra blanca", - "ONLINE": "En línea", - "ADMINISTRATOR": "Administrador", - "MODERATOR": "Moderador", - "NO_REPLIES_FOUND":"No se encontraron respuestas", - "PARTICIPANT": "Participante", - "GENERATING_REPLIES":"Generación de respuestas", - "SUGGEST_A_REPLY":"Sugerir una respuesta", - "GENERATIONG_ICEBREAKER":"Generando rompehielos", - "PUBLIC": "Público", - "PRIVATE": "Privado", - "PASSWORD_PROTECTED": "Protegido con contraseña", - "PRIVACY_AND_SECURITY": "Privacidad y Seguridad", - "PREFERENCES": "Preferencias", - "MEMBERS": "Miembros", - "TODAY": "Hoy", - "YESTERDAY": "Ayer", - "SUNDAY": "domingo", - "MONDAY": "lunes", - "TUESDAY": "martes", - "WEDNESDAY": "miércoles", - "THURSDAY": "jueves", - "FRIDAY": "viernes", - "SATURDAY": "sábado", - "TYPING": "escribiendo...", - "IS_TYPING": "está escribiendo...", - "CLOSE": "Cerrar", - "ENTER_GROUP_NAME": "Introducir nombre de grupo", - "ADD_MEMBERS": "Agregar miembros", - "SEND_MESSAGE": "Enviar mensaje", - "UNBLOCK_USER": "Desbloquear usuario", - "BLOCK_USER": "Bloquear usuario", - "DELETE_AND_EXIT": "Eliminar y salir", - "LEAVE_GROUP": "Salir del grupo", - "CREATE_GROUP": "Crear grupo", - "SHARED_MEDIA": "Medios compartidos", - "VIDEO_CALL": "Videollamada", - "AUDIO_CALL": "Llamada de audio", - "LOADING": "Cargando...", - "REPLY": "responder", - "REPLIES": "respuestas", - "LAUNCH": "Lanzamiento", - "SHARED_COLLABORATIVE_DOCUMENT": "ha compartido un documento colaborativo", - "SHARED_COLLABORATIVE_WHITEBOARD": "ha compartido una pizarra colaborativa", - "CREATED_WHITEBOARD": "Has creado una nueva pizarra colaborativa", - "CREATED_DOCUMENT": "Ha creado un nuevo documento colaborativo", - "PHOTOS": "Fotos", - "VIDEOS": "Vídeos", - "DOCUMENT": "Documento", - "YOU_DELETED_THIS_MESSAGE": "⚠️ Has eliminado este mensaje", - "THIS_MESSAGE_DELETED": "⚠️ Este mensaje fue eliminado", - "VIEW_ON_YOUTUBE": "Ver en Youtube", - "SEARCH": "Buscar", - "NO_USERS_FOUND": "No se han encontrado usuarios", - "ERROR": "Error", - "NO_GROUPS_FOUND": "No se han encontrado grupos", - "NO_CHATS_FOUND": "No se encontraron chats", - "MEDIA_MESSAGE": "Mensaje multimedia", - "INCOMING_AUDIO_CALL": "Llamada de audio entrante", - "INCOMING_VIDEO_CALL": "Videollamada entrante", - "DECLINE": "Declive", - "ACCEPT": "Aceptar", - "CALL_INITIATED": "Llamada iniciada", - "OUTGOING_AUDIO_CALL": "Llamada de audio saliente", - "OUTGOING_VIDEO_CALL": "Videollamada saliente", - "CALL_REJECTED": "Llamada rechazada", - "REJECTED_CALL": "llamada rechazada", - "CALL_ACCEPTED": "Llamada aceptada", - "JOINED": "se unieron", - "LEFT_THE_CALL": "dejó la llamada", - "UNANSWERED_AUDIO_CALL": "Llamada de audio sin respuesta", - "UNANSWERED_VIDEO_CALL": "Videollamada sin respuesta", - "CALL_ENDED": "Llamada finalizada", - "CANCELLED_CALL": "Llamada cancelada", - "CALL_BUSY": "Llamada ocupada", - "CALLING": "Llamando...", - "ADD": "Añadir", - "NO_BANNED_MEMBERS_FOUND": "No se encontraron miembros prohibidos", - "BANNED_MEMBERS": "Miembros prohibidos", - "NAME": "Nombre", - "SCOPE": "Ámbito", - "UNBAN": "Unban la prohibición", - "SELECT_GROUP_TYPE": "Seleccionar tipo de grupo", - "ENTER_GROUP_PASSWORD": "Introducir contraseña de grupo", - "CREATE": "Crear", - "CREATE_POLL": "Crear encuesta", - "QUESTION": "Pregunta", - "ENTER_YOUR_QUESTION": "Introduce tu pregunta", - "OPTIONS": "Opciones", - "ENTER_YOUR_OPTION": "Introduce tu opción", - "ADD_NEW_OPTION": "Agregar nueva opción", - "VIEW_MEMBERS": "Ver miembros", - "DETAILS": "Detalles", - "NOTIFICATIONS": "Notificaciones", - "OTHER": "Otro", - "HELP": "Ayudar", - "REPORT_PROBLEM": "Informar de un problema", - "GROUP_MEMBERS": "Miembros del grupo", - "BAN": "Prohibición", - "KICK": "Patada", - "PICK_YOUR_EMOJI": "Elige tu emoji", - "PRIVATE_GROUP": "Grupo Privado", - "PROTECTED_GROUP": "Grupo protegido", - "VISIT": "Visitar", - "ATTACH": "Adjuntar", - "ATTACH_FILE": "Adjuntar archivo", - "ATTACH_VIDEO": "Adjuntar vídeo", - "ATTACH_AUDIO": "Adjuntar audio", - "ATTACH_IMAGE": "Adjuntar imagen", - "COLLABORATE_USING_DOCUMENT": "Colaborar con un documento", - "COLLABORATE_USING_WHITEBOARD": "Colaborar con una pizarra", - "EMOJI": "Emoji", - "ENTER_YOUR_MESSAGE_HERE": "Introduzca su mensaje aquí", - "NO_MESSAGES_FOUND": "No se han encontrado mensajes", - "THREAD": "Rosca", - "COLLABORATIVE_DOCUMENT": "Documento colaborativo", - "COLLABORATIVE_WHITEBOARD": "Pizarra colaborativa", - "ADD_REACTION": "Añadir reacción", - "NO_STICKERS_FOUND": "No se encontraron pegatinas", - "REPLY_TO_THREAD": "Responder al hilo", - "REPLY_IN_THREAD": "Responder en hilo", - "DELETE_MESSAGE": "Eliminar mensaje", - "EDIT_MESSAGE": "Editar mensaje", - "OWNER": "Propietario", - "CHANGE_SCOPE": "Cambiar ámbito", - "STICKER": "Pegatina", - "LAST_ACTIVE_AT": "Último activo en", - "VOICE_CALL": "Llamada de voz", - "VIEW_DETAIL": "Ver detalle", - "VOTES": "votos", - "VOTE": "Votar", - "NO_VOTE": "Sin voto", - "REACTED": "reaccionó", - "ADDED": "añadido", - "UNBANNED": "no prohibida", - "MADE": "hecho", - "UNANSWERED_CALL": "Llamada sin respuesta", - "MISSED_AUDIO_CALL": "Llamada de audio perdida", - "ENTER_YOUR_PASSWORD": "Introduce tu contraseña", - "DOCS": "Documentos", - "NO_RECORDS_FOUND": "No se encontraron registros", - "LIVE_REACTION": "Reacción en vivo", - "SMILEY_PEOPLE": "Smileys & Gente", - "ANIMALES_NATURE": "Animales y Naturaleza", - "FOOD_DRINK": "Comida y bebida", - "ACTIVITY": "Actividad", - "TRAVEL_PLACES": "Viajes y Lugares", - "OBJECTS": "Objetos", - "SYMBOLS": "Símbolos", - "FLAGS": "Banderas", - "SENT": "Enviado", - "SEEN": "Visto", - "DELIVERED": "Entregado", - "TRANSLATE_MESSAGE": "Traducir mensaje", - "LEFT": "izquierda", - "KICKED": "patadas", - "BANNED": "prohibido", - "NEW_MESSAGES": "mensajes nuevos", - "NEW_MESSAGE": "mensaje nuevo", - "JUMP": "Saltar", - "SELECT_VIDEO_SOURCE": "Seleccionar origen de vídeo", - "SELECT_INPUT_AUDIO_SOURCE": "Seleccionar fuente de audio de entrada", - "SELECT_OUTPUT_AUDIO_SOURCE": "Seleccionar fuente de audio de salida", - "INITIATED_GROUP_CALL": "ha iniciado una llamada de grupo", - "YOU_INITIATED_GROUP_CALL": "Ha iniciado una llamada grupal", - "IGNORE": "Ignorar", - "ON_ANOTHER_CALL": "está en otra llamada", - "CREATING": "Creación", - "AVATAR": "Avatar", - "GROUP_NAME_BLANK": "El nombre del grupo no puede estar en blanco", - "GROUP_TYPE_BLANK": "El tipo de grupo no puede estar en blanco", - "GROUP_PASSWORD_BLANK": "La contraseña de grupo no puede estar en blanco", - "POLL_QUESTION_BLANK": "La pregunta no puede estar en blanco", - "POLL_OPTION_BLANK": "La opción no puede estar en blanco", - "ONGOING_CALL": "Llamada en curso", - "YOU_ALREADY_ONGOING_CALL": "Ya estás en una llamada en curso", - "RESIZE": "Cambiar tamaño", - "SETTINGS": "Configuración", - "ACTIONS": "Acciones", - "VIEW_PROFILE": "Ver perfil", - "SEND_MESSAGE_IN_PRIVATE": "Enviar mensaje en privado", - "DELETE": "Borrar", - "DELETE_CONFIRM": "¿Está seguro de que desea eliminar?", - "CANCEL": "Cancelar", - "LEAVE_CONFIRM": "¿Estás seguro de que quieres dejar el grupo?", - "TRANSFER_CONFIRM": "Usted es el propietario del grupo, por favor transfiera la propiedad a un miembro antes de abandonar el grupo", - "ADDING": "Añadiendo...", - "TRANSFER": "Transferencia", - "TRANSFERRING": "Transferir", - "YES": "Sí", - "NO": "No", - "SOMETHING_WRONG": "Algo salió mal, inténtalo de nuevo", - "INVALID_GROUP_NAME": "Introduce un nombre válido para el grupo e inténtalo de nuevo", - "INVALID_GROUP_TYPE": "Introduce un tipo válido para el grupo e inténtalo de nuevo", - "INVALID_PASSWORD": "Introduce una contraseña válida para el grupo e inténtalo de nuevo", - "WRONG_PASSWORD": "Introduce la contraseña correcta e inténtalo de nuevo", - "INVALID_POLL_QUESTION": "Introduce la pregunta requerida antes de crear una encuesta", - "INVALID_POLL_OPTION": "Introduce la respuesta requerida antes de crear una encuesta", - "SAME_LANGUAGE_MESSAGE": "El idioma seleccionado para la traducción es similar al idioma del mensaje original", - "LEAVE": "Dejar", - "CALLS": "Llamadas", - "CUSTOM_MESSAGE_LOCATION": "📍 Ubicación", - "OFFLINE": "Offline", - "YOU": "Usted", - "PRIVACY": "Privacidad", - "BLOCKED_USERS": "Usuarios bloqueados", - "YOU'VE_BLOCKED": "Has bloqueado", - "NO_PHOTOS": "No hay fotos", - "NO_VIDEOS": "No hay vídeos", - "NO_DOCUMENTS": "Sin documentos", - "JOIN": "Unirse", - "DELETE_CONFIRM_MESSAGE": "¿Desea eliminar esta conversación? Esta conversación se eliminará de todos tus dispositivos.", - "CHAT_ERROR_MESSAGE": "No se pueden cargar los chats. Inténtelo de nuevo", - "TRY_AGAIN": "VUELVE A INTENTARLO", - "CONFIRM": "Confirmar", - "UNSAFE_CONTENT": "CONTENIDO INSEGURO", - "UNSAFE_CONFIRMATION": "¿SEGURO QUE QUIERES VER ESTE CONTENIDO INSEGURO?", - "AUDIO_FILE": "ARCHIVO DE AUDIO", - "SHARED_FILE": "ARCHIVO COMPARTIDO", - "OPEN_DOCUMENT": "ABRIR DOCUMENTO", - "OPEN_WHITEBOARD": "JABALÍ ABIERTO", - "PEOPLE_VOTED": "LA GENTE VOTA", - "ADD_TO_CHAT": "AÑADIR A CHA", - "TAKE_PHOTO": "TOMA UNA FOTO", - "SET_THE_ANSWERS": "ESTABLECE LAS RESPUESTAS", - "ADD_ANOTHER_ANSWER": "AÑADIR OTRA RESPUESTA", - "ANSWER": "RESPUESTA", - "IN_A_THREAD": "En un hilo ⤵", - "ERROR_GROUP_CREATE": "No se puede crear el grupo", - "TRY_AGAIN_LATER": "Vuelve a intentarlo más tarde", - "CONTINUE": "Continuar", - "GROUP_NAME_MAX": "Se permite un máximo de 25 caracteres en el nombre del grupo", - "PASSWORD_MAX": "Se permite un máximo de 16 caracteres en la contraseña de grupo", - "GROUP_PASSWORD": "Contraseña de grupo", - "INCORRECT_PASSWORD": "Contraseña incorrecta", - "OPEN_WHITEBOARD_TO_DRAW": "Abra la pizarra para dibujar juntos", - "CALL_HISTORY": "Historial de llamadas", - "NO_CALL_HISTORY": "Aún no se ha hecho ninguna llamada", - "CALL_DETAILS": "Detalles de la llamada", - "CONFERENCE_CALL": "conferencia telefónica", - "NEW_GROUP": "Nuevo grupo", - "TRANSFER_OWNERSHIP": "Transferir propiedad", - "COPY_MESSAGE": "Copiar", - "SHARE": "Compartir", - "OPEN_DOCUMENT_TO_DRAW": "Abrir documento para dibujar juntos", - "INFORMATION": "Información", - "COPY_TEXT": "Copiar texto", - "FORWARD": "Adelante", - "TEXT": "Texto", - "INCOMING_CALL": "Llamada entrante", - "OUTGOING_CALL": "Llamada saliente", - "MISSED_CALL": "Llamada perdida", - "NEW_CONVERSATION": "Nuevo chat", - "FORWARDING": "Envío de mensajes..", - "READ": "Leer", - "No_RECIPIENT": "Sin destinatario", - "RECEIPT_INFORMATION": "Información del recibo", - "MESSAGE": "Mensaje", - "GENERATING_SUMMARY":"Generación de resumen", - "CONVERSATION_SUMMARY":"Resumen de la conversación", - "GENERATE_SUMMARY":"Generar un resumen", - "COMETCHAT_BOT_FIRST_MESSAGE":"¿Cómo puedo ayudarte con esta conversación? Por favor, hazme una pregunta y te asesoraré 🙂", - "COMETCHAT_ASK_BOT":"Preguntar", - "COMETCHAT_ASK_AI_BOT":"Pregúntale a los bots de IA", - "COMETCHAT_ASK_BOT_SUBTITLE":"Bot de IA", - "FORM_COMPLETION_MESSAGE": "Gracias por rellenar el formulario.", - "FORM":"Formulario", - "CARD":"Tarjeta", - "RECORDINGS":"Grabaciones", - "PARTICIPANTS":"Participantes", - "NO_PARTICIPANTS":"No hay participantes", - "NO_RECORDINGS":"Sin grabaciones", - "CALL_LOGS":"Registros de llamadas", - "CANCELLED_AUDIO_CALL":"Llamada de audio cancelada", - "CANCELLED_VIDEO_CALL":"Videollamada cancelada", - "MICROPHONE_PERMISSION":"Usamos el micrófono para grabar y compartir mensajes de audio. Ve a la configuración para habilitar el acceso al micrófono" -} \ No newline at end of file + "USERS": "Usuarios", + "CHATS": "Charlas", + "GROUPS": "Grupos", + "MORE": "Más", + "MESSAGE_IMAGE": "📷 Imagen", + "MESSAGE_FILE": "📁 Archivo", + "MESSAGE_VIDEO": "📹 Vídeo", + "MESSAGE_AUDIO": "🎵 Audio", + "CUSTOM_MESSAGE": "Tienes un mensaje", + "MISSED_VOICE_CALL": "Llamada de voz perdida", + "MISSED_VIDEO_CALL": "Videollamada perdida", + "CUSTOM_MESSAGE_POLL": "📊 Encuesta", + "CUSTOM_MESSAGE_STICKER": "💟 Pegatina", + "CUSTOM_MESSAGE_DOCUMENT": "📃 Documento", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 Pizarra", + "ONLINE": "En línea", + "ADMINISTRATOR": "Administrador", + "MODERATOR": "presentador", + "PARTICIPANT": "Participante", + "SUGGEST_A_REPLY": "Sugerir una respuesta", + "GENERATIONG_ICEBREAKER": "Generando rompehielos", + "PUBLIC": "Público", + "NO_REPLIES_FOUND": "No se han encontrado respuestas", + "PRIVATE": "Privada", + "PASSWORD_PROTECTED": "Protegido con contraseña", + "PRIVACY_AND_SECURITY": "Privacidad y seguridad", + "PREFERENCES": "Preferencias", + "MEMBERS": "Miembros", + "TODAY": "Hoy", + "YESTERDAY": "Ayer", + "SUNDAY": "domingo", + "MONDAY": "lunes", + "TUESDAY": "martes", + "WEDNESDAY": "miércoles", + "THURSDAY": "jueves", + "FRIDAY": "viernes", + "SATURDAY": "sábado", + "TYPING": "escribiendo...", + "IS_TYPING": "está escribiendo...", + "CLOSE": "Cerrar", + "ENTER_GROUP_NAME": "Introduzca el nombre del grupo", + "ADD_MEMBERS": "Agregar miembros", + "SEND_MESSAGE": "Enviar mensaje", + "UNBLOCK_USER": "Desbloquear usuario", + "BLOCK_USER": "Bloquear usuario", + "DELETE_AND_EXIT": "Eliminar y salir", + "LEAVE_GROUP": "Salir del grupo", + "CREATE_GROUP": "Crear grupo", + "SHARED_MEDIA": "Medios compartidos", + "VIDEO_CALL": "Videollamada", + "AUDIO_CALL": "Llamada de audio", + "LOADING": "Cargando...", + "REPLY": "respuesta", + "REPLIES": "respuestas", + "LAUNCH": "Lanzar", + "SHARED_COLLABORATIVE_DOCUMENT": "ha compartido un documento colaborativo", + "SHARED_COLLABORATIVE_WHITEBOARD": "ha compartido una pizarra colaborativa", + "CREATED_WHITEBOARD": "Ha creado una nueva pizarra colaborativa", + "CREATED_DOCUMENT": "Ha creado un nuevo documento de SOMCollaborative", + "PHOTOS": "Fotos", + "VIDEOS": "Vídeos", + "DOCUMENT": "Documento", + "MESSAGE_IS_DELETED": "El mensaje se ha eliminado", + "THIS_MESSAGE_DELETED": "⚠️ Este mensaje se ha eliminado", + "VIEW_ON_YOUTUBE": "Ver en Youtube", + "SEARCH": "Búsqueda", + "NO_USERS_FOUND": "No se ha encontrado ningún usuario", + "ERROR": "Error", + "NO_GROUPS_FOUND": "No se ha encontrado ningún grupo", + "NO_CHATS_FOUND": "No se han encontrado chats", + "MEDIA_MESSAGE": "Mensaje para los medios", + "INCOMING_AUDIO_CALL": "Llamada de audio entrante", + "INCOMING_VIDEO_CALL": "Videollamada entrante", + "DECLINE": "Declinación", + "ACCEPT": "Aceptar", + "CALL_INITIATED": "Llamada iniciada", + "OUTGOING_AUDIO_CALL": "Llamada de audio saliente", + "OUTGOING_VIDEO_CALL": "Videollamada saliente", + "CALL_REJECTED": "Llamada rechazada", + "REJECTED_CALL": "Llamada rechazada", + "CALL_ACCEPTED": "Llamada aceptada", + "JOINED": "se unió", + "LEFT_THE_CALL": "dejó la llamada", + "UNANSWERED_AUDIO_CALL": "Llamada de audio sin respuesta", + "UNANSWERED_VIDEO_CALL": "Videollamada sin respuesta", + "CALL_ENDED": "Llamada finalizada", + "CANCELLED_CALL": "Llamada cancelada", + "CALL_BUSY": "Llamada ocupada", + "CALLING": "Llamando...", + "ADD": "Añadir", + "NO_BANNED_MEMBERS_FOUND": "No se ha encontrado ningún miembro prohibido", + "BANNED_MEMBERS": "Miembros prohibidos", + "NAME": "Nombre", + "SCOPE": "Alcance", + "UNBAN": "Unban", + "SELECT_GROUP_TYPE": "Seleccione el tipo de grupo", + "ENTER_GROUP_PASSWORD": "Ingresa la contraseña del grupo", + "CREATE": "Crear", + "CREATE_POLL": "Crear encuesta", + "QUESTION": "Pregunta", + "ENTER_YOUR_QUESTION": "Ingresa tu pregunta", + "OPTIONS": "Opciones", + "ENTER_YOUR_OPTION": "Ingresa tu opción", + "ADD_NEW_OPTION": "Añadir nueva opción", + "VIEW_MEMBERS": "Ver miembros", + "DETAILS": "Detalles", + "NOTIFICATIONS": "Notificaciones", + "OTHER": "Otros", + "HELP": "Ayuda", + "REPORT_PROBLEM": "Reportar un problema", + "GROUP_MEMBERS": "Miembros del grupo", + "BAN": "Prohibir", + "KICK": "Patada", + "PICK_YOUR_EMOJI": "Elige tu emoji", + "PRIVATE_GROUP": "Grupo privado", + "PROTECTED_GROUP": "Grupo protegido", + "VISIT": "Visita", + "ATTACH": "Adjuntar", + "ATTACH_FILE": "Adjuntar archivo", + "ATTACH_VIDEO": "Adjuntar vídeo", + "ATTACH_AUDIO": "Adjuntar audio", + "ATTACH_IMAGE": "Adjuntar imagen", + "COLLABORATIVE_DOCUMENT": "Documento colaborativo", + "COLLABORATIVE_WHITEBOARD": "Pizarra colaborativa", + "COLLABORATE_USING_DOCUMENT": "Colabore mediante un documento", + "COLLABORATE_USING_WHITEBOARD": "Colabore mediante una pizarra", + "EMOJI": "Emoji", + "ENTER_YOUR_MESSAGE_HERE": "Introduce tu mensaje aquí", + "NO_MESSAGES_FOUND": "Aún no hay mensajes aquí...", + "THREAD": "Hilo", + "ADD_REACTION": "Añadir reacción", + "NO_STICKERS_FOUND": "No se han encontrado pegatinas", + "REPLY_TO_THREAD": "Responder al hilo", + "REPLY_IN_THREAD": "Responder en hilo", + "DELETE_MESSAGE": "Eliminar mensaje", + "EDIT_MESSAGE": "Editar mensaje", + "OWNER": "Dueño", + "CHANGE_SCOPE": "Cambiar ámbito", + "STICKER": "Adhesivo", + "LAST_ACTIVE_AT": "Último día activo", + "VOICE_CALL": "Llamada de voz", + "VIEW_DETAIL": "Ver detalle", + "VOTES": "vota", + "VOTE": "votar", + "NO_VOTE": "Sin voto", + "REACTED": "reaccionó", + "ADDED": "adicional", + "UNBANNED": "no prohibido", + "MADE": "hecho", + "UNANSWERED_CALL": "Llamada sin respuesta", + "MISSED_AUDIO_CALL": "Llamada de audio perdida", + "ENTER_YOUR_PASSWORD": "Introduce tu contraseña", + "DOCS": "Documentos", + "NO_RECORDS_FOUND": "No se ha encontrado ningún registro", + "LIVE_REACTION": "Reacción en vivo", + "SMILEY_PEOPLE": "Sonrisas y personas", + "ANIMALES_NATURE": "Animales y naturaleza", + "FOOD_DRINK": "Comida y bebida", + "ACTIVITY": "Actividad", + "TRAVEL_PLACES": "Viajes y lugares", + "OBJECTS": "Objetos", + "SYMBOLS": "Símbolos", + "FLAGS": "Banderas", + "SENT": "Enviado", + "SEEN": "Visto", + "DELIVERED": "Entregado", + "TRANSLATE_MESSAGE": "Traducir mensaje", + "LEFT": "izquierda", + "KICKED": "pateado", + "BANNED": "prohibido", + "NEW_MESSAGES": "mensajes nuevos", + "NEW_MESSAGE": "mensaje nuevo", + "JUMP": "Salta", + "SELECT_VIDEO_SOURCE": "Seleccione la fuente de vídeo", + "SELECT_INPUT_AUDIO_SOURCE": "Seleccione la fuente de audio de entrada", + "SELECT_OUTPUT_AUDIO_SOURCE": "Seleccione la fuente de audio de salida", + "INITIATED_GROUP_CALL": "ha iniciado una llamada grupal", + "YOU_INITIATED_GROUP_CALL": "Has iniciado una llamada grupal", + "IGNORE": "Ignorar", + "ON_ANOTHER_CALL": "está en otra llamada", + "CREATING": "Creando", + "AVATAR": "Avatar", + "ONGOING_CALL": "Convocatoria en curso", + "YOU_ALREADY_ONGOING_CALL": "Ya estás en una llamada en curso", + "RESIZE": "Redimensionar", + "SETTINGS": "Ajustes", + "ACTIONS": "Acciones", + "VIEW_PROFILE": "Ver perfil", + "SEND_MESSAGE_IN_PRIVATE": "Enviar mensaje de forma privada", + "DELETE": "Borrar", + "DELETE_CONFIRM": "¿Estás seguro de que quieres eliminarlo?", + "CANCEL": "Cancelar", + "LEAVE_CONFIRM": "¿Estás seguro de que quieres dejar el grupo?", + "TRANSFER_CONFIRM": "Eres el propietario del grupo; transfiere la propiedad a un miembro antes de abandonar el grupo", + "ADDING": "Agregando...", + "TRANSFER": "Traslado", + "TRANSFERRING": "Transferir", + "YES": "Sí", + "NO": "No", + "SOMETHING_WRONG": "Se ha producido un error. Vuelve a intentarlo.", + "INVALID_GROUP_NAME": "Introduce un nombre válido para el grupo e inténtalo de nuevo", + "INVALID_PASSWORD": "Introduce una contraseña válida para el grupo e inténtalo de nuevo", + "INVALID_GROUP_TYPE": "Introduce un tipo válido para el grupo e inténtalo de nuevo", + "WRONG_PASSWORD": "Introduce la contraseña correcta e inténtalo de nuevo", + "INVALID_POLL_QUESTION": "Introduce la pregunta requerida antes de crear una encuesta", + "INVALID_POLL_OPTION": "Ingresa la respuesta requerida antes de crear una encuesta", + "SAME_LANGUAGE_MESSAGE": "El idioma seleccionado para la traducción es similar al idioma del mensaje original", + "LEAVE": "Salir", + "CUSTOM_MESSAGE_LOCATION": "📍 Ubicación", + "IN_A_THREAD": "En un hilo ⤵", + "CALLS": "Convocatorias", + "OFFLINE": "Fuera de línea", + "YOU": "Tú", + "PRIVACY": "Privacidad", + "BLOCKED_USERS": "Usuarios bloqueados", + "YOU'VE_BLOCKED": "Has bloqueado", + "NO_PHOTOS": "No hay fotos", + "NO_VIDEOS": "No hay vídeos", + "NO_DOCUMENTS": "Sin documentos", + "GENERATING_REPLIES": "Generar respuestas", + "JOIN": "Únete", + "DELETE_CONFIRM_MESSAGE": "¿Quieres eliminar esta conversación? Esta conversación se eliminará de todos tus dispositivos.", + "CHAT_ERROR_MESSAGE": "No se pueden cargar los chats. Vuelve a intentarlo", + "TRY_AGAIN": "INTÉNTALO DE NUEVO", + "CONFIRM": "Confirmar", + "UNSAFE_CONTENT": "Contenido inseguro", + "UNSAFE_CONFIRMATION": "¿Seguro que quieres ver este contenido no seguro?", + "AUDIO_FILE": "Archivo de audio", + "SHARED_FILE": "Archivo compartido", + "OPEN_DOCUMENT": "DOCUMENTO ABIERTO", + "OPEN_WHITEBOARD": "PIZARRA ABIERTA", + "PEOPLE_VOTED": "la gente votó", + "ADD_TO_CHAT": "Añadir al chat", + "TAKE_PHOTO": "Toma una foto", + "SET_THE_ANSWERS": "ESTABLECE LAS RESPUESTAS", + "ADD_ANOTHER_ANSWER": "Añadir otra respuesta", + "ANSWER": "Responder", + "ERROR_GROUP_CREATE": "No se puede crear un grupo", + "TRY_AGAIN_LATER": "Vuelva a intentarlo más tarde", + "CONTINUE": "Continuar", + "GROUP_NAME_MAX": "Se permite un máximo de 25 caracteres en el nombre del grupo", + "GROUP_PASSWORD_BLANK": "Introduzca la contraseña", + "PASSWORD_MAX": "Se permite un máximo de 16 caracteres en la contraseña de grupo", + "GROUP_PASSWORD": "Contraseña de grupo", + "INCORRECT_PASSWORD": "Contraseña incorrecta", + "OPEN_WHITEBOARD_TO_DRAW": "Abra la pizarra para dibujar juntos", + "CALL_HISTORY": "Historial de llamadas", + "NO_CALL_HISTORY": "Aún no se ha realizado ninguna llamada", + "CALL_DETAILS": "Detalles de la llamada", + "CONFERENCE_CALL": "conferencia telefónica", + "NEW_GROUP": "Nuevo grupo", + "TRANSFER_OWNERSHIP": "Transferir la propiedad", + "COPY_MESSAGE": "Copiar", + "SHARE": "Compartir", + "OPEN_DOCUMENT_TO_DRAW": "Documento abierto para dibujar juntos", + "INFORMATION": "Información del mensaje", + "FORWARD": "Adelante", + "COPY_TEXT": "Copiar texto", + "TEXT": "Texto", + "INCOMING_CALL": "Llamada entrante", + "OUTGOING_CALL": "Llamada saliente", + "MISSED_CALL": "Llamada perdida", + "GROUP_NAME_BLANK": "El nombre del grupo no debe estar vacío", + "GROUP_TYPE_BLANK": "El tipo de grupo No se puede está vacío", + "POLL_QUESTION_BLANK": "La pregunta canaut está vacía", + "POLL_OPTION_BLANK": "La opción No se puede está vacía", + "NEW_CONVERSATION": "Nuevo chat", + "FORWARDING": "Enviando mensajes...", + "READ": "Leer", + "No_RECIPIENT": "Sin destinatario", + "RECEIPT_INFORMATION": "Información de recibo", + "MESSAGE": "Mensaje", + "GENERATING_SUMMARY": "Generando un resumen", + "CONVERSATION_SUMMARY": "Resumen de la conversación", + "GENERATE_SUMMARY": "Generar un resumen", + "COMETCHAT_BOT_FIRST_MESSAGE": "¿Cómo puedo ayudarlo con esta conversación? Por favor, hazme una pregunta y te asesoraré 🙂", + "COMETCHAT_ASK_BOT": "Preguntar", + "COMETCHAT_ASK_AI_BOT": "Pregúntale a AI Bots", + "COMETCHAT_ASK_BOT_SUBTITLE": "Bot de IA", + "FORM_COMPLETION_MESSAGE": "Gracias por rellenar el formulario.", + "FORM": "Formulario", + "CARD": "Tarjeta", + "RECORDINGS": "Grabaciones", + "PARTICIPANTS": "Participantes", + "NO_PARTICIPANTS": "No hay participantes", + "NO_RECORDINGS": "No hay grabaciones", + "CALL_LOGS": "Registros de llamadas", + "CANCELLED_AUDIO_CALL": "Llamada de audio cancelada", + "CANCELLED_VIDEO_CALL": "Videollamada cancelada", + "MEET_WITH": "Reúnase con", + "MIN_MEETING": "reunión mínima", + "MORE_TIMES": "Más veces", + "SELECT_DAY": "Selecciona un día", + "SELECT_TIME": "Selecciona una hora", + "NO_TIME_SLOT_AVAILABLE": "No hay franja horaria disponible en esta fecha. Por favor, prueba con otra fecha.", + "BOOK_NEW_SLOT": "Reserva un nuevo espacio", + "TRY_AGAIN_CAMEL": "Inténtalo de nuevo", + "TIME_SLOT_UNAVAILABLE": "La franja horaria ya no está disponible. Por favor, elige otro.", + "MEETING_SCHEDULER": "Programador de reuniones", + "MIN": "mi" +} diff --git a/src/shared/resources/CometChatLocalize/resources/fr/translation.json b/src/shared/resources/CometChatLocalize/resources/fr/translation.json index 4813fba..9dbb639 100644 --- a/src/shared/resources/CometChatLocalize/resources/fr/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/fr/translation.json @@ -1,292 +1,302 @@ { - "USERS": "Utilisateurs", - "CHATS": "Discussions", - "GROUPS": "Groupes", - "MORE": "Plus", - "MESSAGE_IMAGE": "📷 Image", - "MESSAGE_FILE": "📁 Fichier", - "MESSAGE_VIDEO": "📹 Vidéo", - "MESSAGE_AUDIO": "🎵 Audio", - "CUSTOM_MESSAGE": "Vous avez un message", - "MISSED_VOICE_CALL": "Appel vocal manqué", - "MISSED_VIDEO_CALL": "Appel vidéo manqué", - "CUSTOM_MESSAGE_POLL": "📊 Sondage", - "CUSTOM_MESSAGE_STICKER": "💟 Autocollant", - "CUSTOM_MESSAGE_DOCUMENT": "📃 Document", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 Tableau blanc", - "ONLINE": "En ligne", - "ADMINISTRATOR": "Administrateur", - "MODERATOR": "Modérateur", - "NO_REPLIES_FOUND":"Aucune réponse trouvée", - "PARTICIPANT": "Participant", - "GENERATING_REPLIES":"Génération de réponses", - "PUBLIC": "Public", - "SUGGEST_A_REPLY":"Suggérer une réponse", - "GENERATIONG_ICEBREAKER":"Génération de brise-glaces", - "PRIVATE": "Privé", - "PASSWORD_PROTECTED": "Mot de passe", - "PRIVACY_AND_SECURITY": "Confidentialité et sécurité", - "PREFERENCES": "Préférences", - "MEMBERS": "Membres", - "TODAY": "Aujourd'hui", - "YESTERDAY": "Hier", - "SUNDAY": "Dimanche", - "MONDAY": "Lundi", - "TUESDAY": "Mardi", - "WEDNESDAY": "Mercredi", - "THURSDAY": "Jeudi", - "FRIDAY": "Vendredi", - "SATURDAY": "Samedi", - "TYPING": "dactylographie...", - "IS_TYPING": "est en train de taper...", - "CLOSE": "Fermer", - "ENTER_GROUP_NAME": "Saisir le nom du groupe", - "ADD_MEMBERS": "Ajouter des membres", - "SEND_MESSAGE": "Envoyer un message", - "UNBLOCK_USER": "Débloquer l'utilisateur", - "BLOCK_USER": "Bloquer l'utilisateur", - "DELETE_AND_EXIT": "Supprimer et quitter", - "LEAVE_GROUP": "Groupe de congé", - "CREATE_GROUP": "Créer un groupe", - "SHARED_MEDIA": "Médias partagés", - "VIDEO_CALL": "Appel vidéo", - "AUDIO_CALL": "Appel audio", - "LOADING": "Chargement...", - "REPLY": "répondre", - "REPLIES": "réponses", - "LAUNCH": "Lancement", - "SHARED_COLLABORATIVE_DOCUMENT": "a partagé un document collaboratif", - "SHARED_COLLABORATIVE_WHITEBOARD": "a partagé un tableau blanc collaboratif", - "CREATED_WHITEBOARD": "Vous avez créé un nouveau tableau blanc collaboratif", - "CREATED_DOCUMENT": "Vous avez créé un nouveau document collaboratif", - "PHOTOS": "Photos", - "VIDEOS": "Vidéos", - "DOCUMENT": "Document", - "YOU_DELETED_THIS_MESSAGE": "⚠️ Vous avez supprimé ce message", - "THIS_MESSAGE_DELETED": "⚠️ Ce message a été supprimé", - "VIEW_ON_YOUTUBE": "Voir sur Youtube", - "SEARCH": "Rechercher", - "NO_USERS_FOUND": "Aucun utilisateur trouvé", - "ERROR": "Erreur", - "NO_GROUPS_FOUND": "Aucun groupe trouvé", - "NO_CHATS_FOUND": "Aucun chat trouvé", - "MEDIA_MESSAGE": "Message pour les médias", - "INCOMING_AUDIO_CALL": "Appel audio entrant", - "INCOMING_VIDEO_CALL": "Appel vidéo entrant", - "DECLINE": "Refuser", - "ACCEPT": "Accepter", - "CALL_INITIATED": "Appel lancé", - "OUTGOING_AUDIO_CALL": "Appel audio sortant", - "OUTGOING_VIDEO_CALL": "Appel vidéo sortant", - "CALL_REJECTED": "Appel rejeté", - "REJECTED_CALL": "appel rejeté", - "CALL_ACCEPTED": "Appel accepté", - "JOINED": "joint", - "LEFT_THE_CALL": "a quitté l'appel", - "UNANSWERED_AUDIO_CALL": "Appel audio sans réponse", - "UNANSWERED_VIDEO_CALL": "Appel vidéo sans réponse", - "CALL_ENDED": "Appel terminé", - "CANCELLED_CALL": "Appel annulé", - "CALL_BUSY": "Appeler occupé", - "CALLING": "Appeler...", - "ADD": "Ajouter", - "NO_BANNED_MEMBERS_FOUND": "Aucun membre interdit n'a été trouvé", - "BANNED_MEMBERS": "Membres interdits", - "NAME": "Nom", - "SCOPE": "Portée", - "UNBAN": "Unban", - "SELECT_GROUP_TYPE": "Sélectionner le type de groupe", - "ENTER_GROUP_PASSWORD": "Saisir le mot de passe", - "CREATE": "Créer", - "CREATE_POLL": "Créer un sondage", - "QUESTION": "Question", - "ENTER_YOUR_QUESTION": "Saisissez votre question", - "OPTIONS": "Options", - "ENTER_YOUR_OPTION": "Saisissez votre option", - "ADD_NEW_OPTION": "Ajouter une nouvelle option", - "VIEW_MEMBERS": "Afficher les membres", - "DETAILS": "Détails", - "NOTIFICATIONS": "Notifications", - "OTHER": "Autres", - "HELP": "Aide", - "REPORT_PROBLEM": "Signaler un problème", - "GROUP_MEMBERS": "Membres du groupe", - "BAN": "Interdiction", - "KICK": "Coup de pied", - "PICK_YOUR_EMOJI": "Choisissez vos emoji", - "PRIVATE_GROUP": "Groupe privé", - "PROTECTED_GROUP": "Groupe protégé", - "VISIT": "Visitez", - "ATTACH": "Attacher", - "ATTACH_FILE": "Joindre le fichier", - "ATTACH_VIDEO": "Joindre une vidéo", - "ATTACH_AUDIO": "Attacher audio", - "ATTACH_IMAGE": "Joindre l'image", - "COLLABORATE_USING_DOCUMENT": "Collaborer à l'aide d'un document", - "COLLABORATE_USING_WHITEBOARD": "Collaborez à l'aide d'un tableau blanc", - "EMOJI": "Emoji", - "ENTER_YOUR_MESSAGE_HERE": "Entrez votre message ici", - "NO_MESSAGES_FOUND": "Aucun message trouvé", - "THREAD": "Fil", - "COLLABORATIVE_DOCUMENT": "Document collaboratif", - "COLLABORATIVE_WHITEBOARD": "Tableau blanc collaboratif", - "ADD_REACTION": "Ajouter une réaction", - "NO_STICKERS_FOUND": "Aucun autocollant trouvé", - "REPLY_TO_THREAD": "Répondre au fil", - "REPLY_IN_THREAD": "Répondre dans le thread", - "DELETE_MESSAGE": "Supprimer un message", - "EDIT_MESSAGE": "Modifier le message", - "OWNER": "Propriétaire", - "CHANGE_SCOPE": "Modifier l'étendue", - "STICKER": "Autocollant", - "LAST_ACTIVE_AT": "Dernier actif à", - "VOICE_CALL": "Appel vocal", - "VIEW_DETAIL": "Afficher les détails", - "VOTES": "votes", - "VOTE": "vote", - "NO_VOTE": "Pas de vote", - "REACTED": "réagissait", - "ADDED": "ajoutée", - "UNBANNED": "non interdite", - "MADE": "confectionné", - "UNANSWERED_CALL": "Appel sans réponse", - "MISSED_AUDIO_CALL": "Appel audio manqué", - "ENTER_YOUR_PASSWORD": "Entrez votre mot de passe", - "DOCS": "Docs", - "NO_RECORDS_FOUND": "Aucun enregistrement trouvé", - "LIVE_REACTION": "Réaction en direct", - "SMILEY_PEOPLE": "Smileys & Personnes", - "ANIMALES_NATURE": "Animaux & Nature", - "FOOD_DRINK": "Nourriture et boissons", - "ACTIVITY": "Activité", - "TRAVEL_PLACES": "Voyages & Lieux", - "OBJECTS": "Objets", - "SYMBOLS": "Symboles", - "FLAGS": "Drapeaux", - "SENT": "Envoyé", - "SEEN": "Vu", - "DELIVERED": "Livré", - "TRANSLATE_MESSAGE": "Traduire message", - "LEFT": "parti", - "KICKED": "coup de pied", - "BANNED": "interdit", - "NEW_MESSAGES": "nouveaux messages", - "NEW_MESSAGE": "nouveau message", - "JUMP": "Sauter", - "SELECT_VIDEO_SOURCE": "Sélectionner la source vidéo", - "SELECT_INPUT_AUDIO_SOURCE": "Sélectionner la source audio d'entrée", - "SELECT_OUTPUT_AUDIO_SOURCE": "Sélectionner la source audio de sortie", - "INITIATED_GROUP_CALL": "a lancé un appel de groupe", - "YOU_INITIATED_GROUP_CALL": "Vous avez lancé un appel de groupe", - "IGNORE": "Ignore", - "ON_ANOTHER_CALL": "est sur un autre appel", - "CREATING": "Création", - "AVATAR": "Avatar", - "GROUP_NAME_BLANK": "Le nom du groupe ne peut pas être vide", - "GROUP_TYPE_BLANK": "Le type de groupe ne peut pas être vide", - "GROUP_PASSWORD_BLANK": "Le mot de passe du groupe ne peut pas être vide", - "POLL_QUESTION_BLANK": "La question ne peut pas être vide", - "POLL_OPTION_BLANK": "Option ne peut pas être vide", - "ONGOING_CALL": "Appel en cours", - "YOU_ALREADY_ONGOING_CALL": "Vous êtes déjà en cours d'appel", - "RESIZE": "Redimensionner", - "SETTINGS": "Paramètres", - "ACTIONS": "Actions", - "VIEW_PROFILE": "Voir le profil", - "SEND_MESSAGE_IN_PRIVATE": "Envoyer un message privately", - "DELETE": "Supprimer", - "DELETE_CONFIRM": "Êtes-vous sûr de vouloir supprimer ?", - "CANCEL": "Annuler", - "LEAVE_CONFIRM": "Êtes-vous sûr de vouloir quitter le groupe ?", - "TRANSFER_CONFIRM": "Vous êtes le propriétaire du groupe, veuillez transférer la propriété à un membre avant de quitter le groupe.", - "ADDING": "Ajout de...", - "TRANSFER": "Transfert", - "TRANSFERRING": "Transfert", - "YES": "Oui", - "NO": "Non", - "SOMETHING_WRONG": "Quelque chose s'est mal passé, veuillez réessayer", - "INVALID_GROUP_NAME": "Veuillez saisir un nom valide pour le groupe et réessayer.", - "INVALID_GROUP_TYPE": "Veuillez saisir un type valide pour le groupe, puis réessayer", - "INVALID_PASSWORD": "Veuillez saisir un mot de passe valide pour le groupe, puis réessayer", - "WRONG_PASSWORD": "Veuillez saisir le bon mot de passe et réessayer", - "INVALID_POLL_QUESTION": "Veuillez saisir la question requise avant de créer un sondage", - "INVALID_POLL_OPTION": "Veuillez saisir la réponse requise avant de créer un sondage", - "SAME_LANGUAGE_MESSAGE": "La langue sélectionnée pour la traduction est similaire à la langue du message d'origine", - "LEAVE": "Partez", - "CALLS": "Appels", - "CUSTOM_MESSAGE_LOCATION": "📍 Emplacement", - "OFFLINE": "Hors ligne", - "YOU": "Vous", - "PRIVACY": "Vie privée", - "BLOCKED_USERS": "Utilisateurs bloqués", - "YOU'VE_BLOCKED": "Vous avez bloqué", - "NO_PHOTOS": "Pas de photos", - "NO_VIDEOS": "Pas de vidéos", - "NO_DOCUMENTS": "Aucun document", - "JOIN": "Rejoignez", - "DELETE_CONFIRM_MESSAGE": "Souhaitez-vous supprimer cette conversation ? Cette conversation sera supprimée de tous vos appareils.", - "CHAT_ERROR_MESSAGE": "Impossible de charger les discussions. Veuillez réessayer", - "TRY_AGAIN": "ESSAYEZ À NOUVEAU", - "CONFIRM": "Confirmer", - "UNSAFE_CONTENT": "CONTENU DANGEREUX", - "UNSAFE_CONFIRMATION": "VOULEZ-VOUS VRAIMENT VOIR CE CONTENU DANGEREUX", - "AUDIO_FILE": "FICHIER AUDIO", - "SHARED_FILE": "FICHIER PARTAGÉ", - "OPEN_DOCUMENT": "OUVRIR UN DOCUMENT", - "OPEN_WHITEBOARD": "TABLEAU BLANC OUVERT", - "PEOPLE_VOTED": "LES GENS VOTENT", - "ADD_TO_CHAT": "AJOUTER À CHA", - "TAKE_PHOTO": "PRENDRE UNE PHOTO", - "SET_THE_ANSWERS": "DÉFINISSEZ LES RÉPONSES", - "ADD_ANOTHER_ANSWER": "AJOUTER UNE AUTRE RÉPONSE", - "ANSWER": "RÉPONSE", - "IN_A_THREAD": "Dans un fil ⤵", - "ERROR_GROUP_CREATE": "Impossible de créer un groupe", - "TRY_AGAIN_LATER": "Veuillez réessayer ultérieurement", - "CONTINUE": "Poursuivre", - "GROUP_NAME_MAX": "25 caractères maximum autorisés dans le nom du groupe", - "PASSWORD_MAX": "Un maximum de 16 caractères est autorisé dans le mot de passe du groupe", - "GROUP_PASSWORD": "Mot de passe du groupe", - "INCORRECT_PASSWORD": "Mot de passe incorrect", - "OPEN_WHITEBOARD_TO_DRAW": "Ouvrez le tableau blanc pour dessiner ensemble", - "CALL_HISTORY": "Historique des appels", - "NO_CALL_HISTORY": "Aucun appel n'a encore été passé", - "CALL_DETAILS": "Détails de l'appel", - "CONFERENCE_CALL": "conférence téléphonique", - "NEW_GROUP": "Nouveau groupe", - "TRANSFER_OWNERSHIP": "Transférer la propriété", - "COPY_MESSAGE": "Copie", - "SHARE": "Partager", - "OPEN_DOCUMENT_TO_DRAW": "Ouvrez le document pour le dessiner ensemble", - "INFORMATION": "Informations", - "COPY_TEXT": "Copier le texte", - "FORWARD": "Vers l'avant", - "TEXT": "Texte", - "INCOMING_CALL": "Appel entrant", - "OUTGOING_CALL": "Appel sortant", - "MISSED_CALL": "Appel manqué", - "NEW_CONVERSATION": "Nouveau chat", - "FORWARDING": "Envoi de messages.", - "READ": "Lisez", - "No_RECIPIENT": "Aucun destinataire", - "RECEIPT_INFORMATION": "Informations sur le reçu", - "MESSAGE": "Un message", - "GENERATING_SUMMARY":"Génération d'un résumé", - "CONVERSATION_SUMMARY":"Résumé de la conversation", - "GENERATE_SUMMARY":"Générez un résumé", - "COMETCHAT_BOT_FIRST_MESSAGE":"Comment puis-je vous aider dans cette conversation ? Posez-moi une question et je vous conseillerai 🙂", - "COMETCHAT_ASK_BOT":"Demandez", - "COMETCHAT_ASK_AI_BOT":"Demandez à AI Bots", - "COMETCHAT_ASK_BOT_SUBTITLE":"Robot IA", - "FORM_COMPLETION_MESSAGE": "Merci d'avoir rempli le formulaire.", - "FORM":"Formulaire", - "CARD":"Carte", - "RECORDINGS":"Enregistrements", - "PARTICIPANTS":"Participants", - "NO_PARTICIPANTS":"Aucun participant", - "NO_RECORDINGS":"Aucun enregistrement", - "CALL_LOGS":"Journaux d'appels", - "CANCELLED_AUDIO_CALL":"Appel audio annulé", - "CANCELLED_VIDEO_CALL":"Appel vidéo annulé", - "MICROPHONE_PERMISSION":"Nous utilisons un microphone pour enregistrer et partager des messages audio. Accédez aux paramètres pour activer l'accès au microphone" -} \ No newline at end of file + "USERS": "Les utilisateurs", + "CHATS": "Discussions", + "GROUPS": "Groupes", + "MORE": "Plus", + "MESSAGE_IMAGE": "📷 Photo", + "MESSAGE_FILE": "📁 Dossier", + "MESSAGE_VIDEO": "📹 Vidéo", + "MESSAGE_AUDIO": "🎵 Audio", + "CUSTOM_MESSAGE": "Vous avez un message", + "MISSED_VOICE_CALL": "Appel vocal manqué", + "MISSED_VIDEO_CALL": "Appel vidéo manqué", + "CUSTOM_MESSAGE_POLL": "📊 Sondage", + "CUSTOM_MESSAGE_STICKER": "💟 Autocollant", + "CUSTOM_MESSAGE_DOCUMENT": "📃 Dossier", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 Tableau blanc", + "ONLINE": "En ligne", + "ADMINISTRATOR": "Administrateur", + "MODERATOR": "présentateur", + "PARTICIPANT": "Participant", + "SUGGEST_A_REPLY": "Suggérer une réponse", + "GENERATIONG_ICEBREAKER": "Génération de brise-glaces", + "PUBLIC": "Publique", + "NO_REPLIES_FOUND": "Aucune réponse trouvée", + "PRIVATE": "Privé", + "PASSWORD_PROTECTED": "Protection par mot de passe", + "PRIVACY_AND_SECURITY": "Confidentialité et sécurité", + "PREFERENCES": "Préférences", + "MEMBERS": "Membres", + "TODAY": "Aujourd'hui", + "YESTERDAY": "Hier", + "SUNDAY": "dimanche", + "MONDAY": "Lundi", + "TUESDAY": "Mardi", + "WEDNESDAY": "Mercredi", + "THURSDAY": "Jeudi", + "FRIDAY": "Vendredi", + "SATURDAY": "samedi", + "TYPING": "dactylographie...", + "IS_TYPING": "est en train de taper...", + "CLOSE": "Fermer", + "ENTER_GROUP_NAME": "Entrez le nom du groupe", + "ADD_MEMBERS": "Ajouter des membres", + "SEND_MESSAGE": "Envoyer un message", + "UNBLOCK_USER": "Débloquer un utilisateur", + "BLOCK_USER": "Bloquer l'utilisateur", + "DELETE_AND_EXIT": "Supprimer et quitter", + "LEAVE_GROUP": "Quitter le groupe", + "CREATE_GROUP": "Créer un groupe", + "SHARED_MEDIA": "Médias partagés", + "VIDEO_CALL": "Appel vidéo", + "AUDIO_CALL": "Appel audio", + "LOADING": "Chargement en cours...", + "REPLY": "répondre", + "REPLIES": "réponses", + "LAUNCH": "Lancement", + "SHARED_COLLABORATIVE_DOCUMENT": "a partagé un document collaboratif", + "SHARED_COLLABORATIVE_WHITEBOARD": "a partagé un tableau blanc collaboratif", + "CREATED_WHITEBOARD": "Vous avez créé un nouveau tableau blanc collaboratif", + "CREATED_DOCUMENT": "Vous avez créé un nouveau document SOMCollaborative", + "PHOTOS": "Des photos", + "VIDEOS": "Vidéos", + "DOCUMENT": "Document", + "MESSAGE_IS_DELETED": "Le message est supprimé", + "THIS_MESSAGE_DELETED": "⚠️ Ce message a été supprimé", + "VIEW_ON_YOUTUBE": "Voir sur Youtube", + "SEARCH": "Rechercher", + "NO_USERS_FOUND": "Aucun utilisateur n'a été trouvé", + "ERROR": "Erreur", + "NO_GROUPS_FOUND": "Aucun groupe n'a été trouvé", + "NO_CHATS_FOUND": "Aucune discussion trouvée", + "MEDIA_MESSAGE": "Message aux médias", + "INCOMING_AUDIO_CALL": "Appel audio entrant", + "INCOMING_VIDEO_CALL": "Appel vidéo entrant", + "DECLINE": "Déclin", + "ACCEPT": "Accepter", + "CALL_INITIATED": "Appel lancé", + "OUTGOING_AUDIO_CALL": "Appel audio sortant", + "OUTGOING_VIDEO_CALL": "Appel vidéo sortant", + "CALL_REJECTED": "Appel rejeté", + "REJECTED_CALL": "Appel rejeté", + "CALL_ACCEPTED": "Appel accepté", + "JOINED": "joint", + "LEFT_THE_CALL": "a quitté l'appel", + "UNANSWERED_AUDIO_CALL": "Appel audio sans réponse", + "UNANSWERED_VIDEO_CALL": "Appel vidéo sans réponse", + "CALL_ENDED": "L'appel est terminé", + "CANCELLED_CALL": "Appel annulé", + "CALL_BUSY": "Appel occupé", + "CALLING": "J'appelle...", + "ADD": "Ajouter", + "NO_BANNED_MEMBERS_FOUND": "Aucun membre banni n'a été trouvé", + "BANNED_MEMBERS": "Membres bannis", + "NAME": "Nom", + "SCOPE": "Portée", + "UNBAN": "Unban", + "SELECT_GROUP_TYPE": "Sélectionnez le type de groupe", + "ENTER_GROUP_PASSWORD": "Entrez le mot de passe du groupe", + "CREATE": "Créez", + "CREATE_POLL": "Créer un sondage", + "QUESTION": "Question", + "ENTER_YOUR_QUESTION": "Entrez votre question", + "OPTIONS": "Options", + "ENTER_YOUR_OPTION": "Entrez votre option", + "ADD_NEW_OPTION": "Ajouter une nouvelle option", + "VIEW_MEMBERS": "Afficher les membres", + "DETAILS": "Détails", + "NOTIFICATIONS": "Notifications", + "OTHER": "Autres", + "HELP": "Aide", + "REPORT_PROBLEM": "Signaler un problème", + "GROUP_MEMBERS": "Membres du groupe", + "BAN": "Interdiction", + "KICK": "Kick", + "PICK_YOUR_EMOJI": "Choisissez votre emoji", + "PRIVATE_GROUP": "Groupe privé", + "PROTECTED_GROUP": "Groupe protégé", + "VISIT": "Visitez", + "ATTACH": "Joindre", + "ATTACH_FILE": "Joindre un fichier", + "ATTACH_VIDEO": "Joindre une vidéo", + "ATTACH_AUDIO": "Joindre un fichier audio", + "ATTACH_IMAGE": "Joindre une image", + "COLLABORATIVE_DOCUMENT": "Document collaboratif", + "COLLABORATIVE_WHITEBOARD": "Tableau blanc collaboratif", + "COLLABORATE_USING_DOCUMENT": "Collaborez à l'aide d'un document", + "COLLABORATE_USING_WHITEBOARD": "Collaborez à l'aide d'un tableau blanc", + "EMOJI": "Emoji", + "ENTER_YOUR_MESSAGE_HERE": "Entrez votre message ici", + "NO_MESSAGES_FOUND": "Aucun message ici pour l'instant...", + "THREAD": "fil", + "ADD_REACTION": "Ajouter une réaction", + "NO_STICKERS_FOUND": "Aucun autocollant n'a été trouvé", + "REPLY_TO_THREAD": "Répondre au fil", + "REPLY_IN_THREAD": "Répondre dans le fil", + "DELETE_MESSAGE": "Supprimer le message", + "EDIT_MESSAGE": "Modifier le message", + "OWNER": "Propriétaire", + "CHANGE_SCOPE": "Modifier la portée", + "STICKER": "Autocollant", + "LAST_ACTIVE_AT": "Dernière activité", + "VOICE_CALL": "Appel vocal", + "VIEW_DETAIL": "Afficher les détails", + "VOTES": "votes", + "VOTE": "vote", + "NO_VOTE": "Pas de vote", + "REACTED": "réagissait", + "ADDED": "ajoutée", + "UNBANNED": "non banni", + "MADE": "confectionné", + "UNANSWERED_CALL": "Appel sans réponse", + "MISSED_AUDIO_CALL": "Appel audio manqué", + "ENTER_YOUR_PASSWORD": "Entrez votre mot de passe", + "DOCS": "Docs", + "NO_RECORDS_FOUND": "Aucun enregistrement n'a été trouvé", + "LIVE_REACTION": "Réaction en direct", + "SMILEY_PEOPLE": "Smileys et personnages", + "ANIMALES_NATURE": "Animaux et nature", + "FOOD_DRINK": "Nourriture et boisson", + "ACTIVITY": "Activité", + "TRAVEL_PLACES": "Voyages et lieux", + "OBJECTS": "Objets", + "SYMBOLS": "Symboles", + "FLAGS": "Drapeaux", + "SENT": "Envoyé", + "SEEN": "Vu", + "DELIVERED": "Livré", + "TRANSLATE_MESSAGE": "Traduire le message", + "LEFT": "parti", + "KICKED": "coup de pied", + "BANNED": "interdit", + "NEW_MESSAGES": "nouveaux messages", + "NEW_MESSAGE": "nouveau message", + "JUMP": "Sauter", + "SELECT_VIDEO_SOURCE": "Sélectionnez la source vidéo", + "SELECT_INPUT_AUDIO_SOURCE": "Sélectionnez la source audio d'entrée", + "SELECT_OUTPUT_AUDIO_SOURCE": "Sélectionnez la source audio de sortie", + "INITIATED_GROUP_CALL": "a lancé un appel de groupe", + "YOU_INITIATED_GROUP_CALL": "Vous avez lancé un appel de groupe", + "IGNORE": "Ignorer", + "ON_ANOTHER_CALL": "est sur un autre appel", + "CREATING": "Création", + "AVATAR": "Avatar", + "ONGOING_CALL": "Appel en cours", + "YOU_ALREADY_ONGOING_CALL": "Vous êtes déjà inscrit à un appel en cours", + "RESIZE": "Redimensionner", + "SETTINGS": "Réglages", + "ACTIONS": "Actions", + "VIEW_PROFILE": "Afficher le profil", + "SEND_MESSAGE_IN_PRIVATE": "Envoyer un message en privé", + "DELETE": "Supprimer", + "DELETE_CONFIRM": "Êtes-vous sûr de vouloir supprimer ?", + "CANCEL": "Annuler", + "LEAVE_CONFIRM": "Es-tu sûr de vouloir quitter le groupe ?", + "TRANSFER_CONFIRM": "Vous êtes le propriétaire du groupe ; veuillez transférer la propriété à un membre avant de quitter le groupe", + "ADDING": "Ajouter...", + "TRANSFER": "Transfert", + "TRANSFERRING": "Transférer", + "YES": "Oui", + "NO": "Non", + "SOMETHING_WRONG": "Une erreur s'est produite. Veuillez réessayer.", + "INVALID_GROUP_NAME": "Entrez un nom valide pour le groupe et réessayez", + "INVALID_PASSWORD": "Entrez un mot de passe valide pour le groupe et réessayez", + "INVALID_GROUP_TYPE": "Entrez un type valide pour le groupe et réessayez", + "WRONG_PASSWORD": "Entrez le mot de passe correct et réessayez", + "INVALID_POLL_QUESTION": "Veuillez saisir la question requise avant de créer un sondage", + "INVALID_POLL_OPTION": "Veuillez saisir la réponse requise avant de créer un sondage", + "SAME_LANGUAGE_MESSAGE": "La langue sélectionnée pour la traduction est similaire à la langue du message d'origine", + "LEAVE": "Partir", + "CUSTOM_MESSAGE_LOCATION": "📍 Lieu", + "IN_A_THREAD": "Dans un fil ⤵", + "CALLS": "Appels", + "OFFLINE": "Hors ligne", + "YOU": "Vous", + "PRIVACY": "Confidentialité", + "BLOCKED_USERS": "Utilisateurs bloqués", + "YOU'VE_BLOCKED": "Vous avez bloqué", + "NO_PHOTOS": "Pas de photos", + "NO_VIDEOS": "Pas de vidéos", + "NO_DOCUMENTS": "Aucun document", + "GENERATING_REPLIES": "Génération de réponses", + "JOIN": "Joignez-vous", + "DELETE_CONFIRM_MESSAGE": "Désirez-vous supprimer cette conversation ? Cette conversation sera supprimée de tous vos appareils.", + "CHAT_ERROR_MESSAGE": "Impossible de charger les chats. Veuillez réessayer", + "TRY_AGAIN": "ESSAYEZ À NOUVEAU", + "CONFIRM": "Confirmer", + "UNSAFE_CONTENT": "Contenu non sécurisé", + "UNSAFE_CONFIRMATION": "Voulez-vous sûrement voir ce contenu dangereux", + "AUDIO_FILE": "Fichier audio", + "SHARED_FILE": "Fichier partagé", + "OPEN_DOCUMENT": "DOCUMENT OUVERT", + "OPEN_WHITEBOARD": "TABLEAU BLANC OUVERT", + "PEOPLE_VOTED": "les gens ont voté", + "ADD_TO_CHAT": "Ajouter au chat", + "TAKE_PHOTO": "Prendre une photo", + "SET_THE_ANSWERS": "DÉFINISSEZ LES RÉPONSES", + "ADD_ANOTHER_ANSWER": "Ajouter une autre réponse", + "ANSWER": "Réponse", + "ERROR_GROUP_CREATE": "Impossible de créer un groupe", + "TRY_AGAIN_LATER": "Veuillez réessayer plus tard", + "CONTINUE": "Continuer", + "GROUP_NAME_MAX": "25 caractères maximum autorisés dans le nom du groupe", + "GROUP_PASSWORD_BLANK": "Veuillez saisir le mot de passe", + "PASSWORD_MAX": "16 caractères maximum autorisés dans le mot de passe du groupe", + "GROUP_PASSWORD": "Mot de passe du groupe", + "INCORRECT_PASSWORD": "Mot de passe incorrect", + "OPEN_WHITEBOARD_TO_DRAW": "Ouvrez le tableau blanc pour dessiner ensemble", + "CALL_HISTORY": "Historique des appels", + "NO_CALL_HISTORY": "Aucun appel n'a encore été passé", + "CALL_DETAILS": "Détails de l'appel", + "CONFERENCE_CALL": "conférence téléphonique", + "NEW_GROUP": "Nouveau groupe", + "TRANSFER_OWNERSHIP": "Transférer la propriété", + "COPY_MESSAGE": "Copier", + "SHARE": "PARTAGEZ", + "OPEN_DOCUMENT_TO_DRAW": "Ouvrir un document pour dessiner ensemble", + "INFORMATION": "Informations sur le message", + "FORWARD": "Vers l'avant", + "COPY_TEXT": "Copier le texte", + "TEXT": "Texte", + "INCOMING_CALL": "Appel entrant", + "OUTGOING_CALL": "Appel sortant", + "MISSED_CALL": "Appel manqué", + "GROUP_NAME_BLANK": "Le nom du groupe ne doit pas être vide", + "GROUP_TYPE_BLANK": "Le type de groupe Impossible est vide", + "POLL_QUESTION_BLANK": "Le canaut de la question est vide", + "POLL_OPTION_BLANK": "L'option Impossible est vide", + "NEW_CONVERSATION": "Nouveau chat", + "FORWARDING": "Envoi de messages...", + "READ": "Lisez", + "No_RECIPIENT": "Aucun destinataire", + "RECEIPT_INFORMATION": "Informations sur le reçu", + "MESSAGE": "Message", + "GENERATING_SUMMARY": "Génération d'un résumé", + "CONVERSATION_SUMMARY": "Résumé de la conversation", + "GENERATE_SUMMARY": "Générer un résumé", + "COMETCHAT_BOT_FIRST_MESSAGE": "Comment puis-je vous aider dans cette conversation ? N'hésitez pas à me poser une question et je vous conseillerai 🙂", + "COMETCHAT_ASK_BOT": "Demandez", + "COMETCHAT_ASK_AI_BOT": "Demandez à AI Bots", + "COMETCHAT_ASK_BOT_SUBTITLE": "Robot IA", + "FORM_COMPLETION_MESSAGE": "Merci d'avoir rempli le formulaire.", + "FORM": "Formulaire", + "CARD": "Carte", + "RECORDINGS": "Enregistrements", + "PARTICIPANTS": "Les participants", + "NO_PARTICIPANTS": "Aucun participant", + "NO_RECORDINGS": "Aucun enregistrement", + "CALL_LOGS": "Journaux d'appels", + "CANCELLED_AUDIO_CALL": "Appel audio annulé", + "CANCELLED_VIDEO_CALL": "Appel vidéo annulé", + "MEET_WITH": "Rencontrez", + "MIN_MEETING": "réunion minimale", + "MORE_TIMES": "Plus de fois", + "SELECT_DAY": "Sélectionnez un jour", + "SELECT_TIME": "Sélectionnez une heure", + "NO_TIME_SLOT_AVAILABLE": "Aucun créneau horaire n'est disponible à cette date. Veuillez essayer une autre date.", + "BOOK_NEW_SLOT": "Réservez un nouveau créneau", + "TRY_AGAIN_CAMEL": "Essayez à nouveau", + "TIME_SLOT_UNAVAILABLE": "Le créneau horaire n'est plus disponible. Veuillez en choisir un autre.", + "MEETING_SCHEDULER": "Planificateur de réunions", + "MIN": "mi" +} diff --git a/src/shared/resources/CometChatLocalize/resources/hi/translation.json b/src/shared/resources/CometChatLocalize/resources/hi/translation.json index e0da2f7..4e1eb83 100644 --- a/src/shared/resources/CometChatLocalize/resources/hi/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/hi/translation.json @@ -1,292 +1,302 @@ { - "USERS": "उपयोक्ता", - "CHATS": "चैट", - "GROUPS": "समूह", - "MORE": "अधिक", - "MESSAGE_IMAGE": "📷 छवि", - "MESSAGE_FILE": "📁 फ़ाइल", - "MESSAGE_VIDEO": "📹 वीडियो", - "MESSAGE_AUDIO": "🎵 ऑडियो", - "CUSTOM_MESSAGE": "आपके पास एक संदेश है", - "MISSED_VOICE_CALL": "मिस्ड वॉयस कॉल", - "MISSED_VIDEO_CALL": "मिस्ड वीडियो कॉल", - "CUSTOM_MESSAGE_POLL": "📊 पोल", - "CUSTOM_MESSAGE_STICKER": "💟 स्टीकर", - "CUSTOM_MESSAGE_DOCUMENT": "📃 दस्तावेज़", - "SUGGEST_A_REPLY":"जवाब सुझाएं", - "GENERATIONG_ICEBREAKER":"आइसब्रेकर उत्पन्न करना", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 व्हाइटबोर्ड", - "ONLINE": "ऑनलाइन", - "NO_REPLIES_FOUND":"कोई जवाब नहीं मिला", - "ADMINISTRATOR": "प्रशासक", - "MODERATOR": "मॉडरेटर", - "PARTICIPANT": "प्रतिभागी", - "PUBLIC": "पब्लिक", - "GENERATING_REPLIES":"जवाब जनरेट करना", - "PRIVATE": "निजी", - "PASSWORD_PROTECTED": "कूटशब्द सुरक्षित", - "PRIVACY_AND_SECURITY": "गोपनीयता और सुरक्षा", - "PREFERENCES": "प्राथमिकताएं", - "MEMBERS": "सदस्य", - "TODAY": "आज", - "YESTERDAY": "कल", - "SUNDAY": "रविवार", - "MONDAY": "सोमवार", - "TUESDAY": "मंगलवार", - "WEDNESDAY": "बुधवार", - "THURSDAY": "गुरुवार", - "FRIDAY": "शुक्रवार", - "SATURDAY": "शनिवार", - "TYPING": "टाइपिंग...", - "IS_TYPING": "टाइप कर रहा है...", - "CLOSE": "बंद करें", - "ENTER_GROUP_NAME": "समूह नाम भरें", - "ADD_MEMBERS": "सदस्य जोड़ें", - "SEND_MESSAGE": "संदेश भेजें", - "UNBLOCK_USER": "उपयोगकर्ता अनवरोधित करें", - "BLOCK_USER": "अवरोधित उपयोक्ता", - "DELETE_AND_EXIT": "मिटाएँ और बाहर निकलें", - "LEAVE_GROUP": "समूह छोड़ें", - "CREATE_GROUP": "समूह बनाएँ", - "SHARED_MEDIA": "साझा मीडिया", - "VIDEO_CALL": "वीडियो कॉल", - "AUDIO_CALL": "ऑडियो कॉल", - "LOADING": "लोड हो रहा है...", - "REPLY": "उत्तर", - "REPLIES": "उत्तर", - "LAUNCH": "लांच", - "SHARED_COLLABORATIVE_DOCUMENT": "ने एक सहयोगी दस्तावेज़ साझा किया है", - "SHARED_COLLABORATIVE_WHITEBOARD": "ने एक सहयोगी व्हाइटबोर्ड साझा किया है", - "CREATED_WHITEBOARD": "आपने एक नया सहयोगी व्हाइटबोर्ड बनाया है", - "CREATED_DOCUMENT": "आपने एक नया सहयोगी दस्तावेज़ बनाया है", - "PHOTOS": "तस्वीरें", - "VIDEOS": "वीडियो", - "DOCUMENT": "दस्तावेज़", - "YOU_DELETED_THIS_MESSAGE": "⚠️ आपने यह संदेश हटा दिया है", - "THIS_MESSAGE_DELETED": "⚠️ यह संदेश मिटाया गया था", - "VIEW_ON_YOUTUBE": "यूट्यूब पर देखें", - "SEARCH": "खोज", - "NO_USERS_FOUND": "कोई उपयोक्ता नहीं मिला", - "ERROR": "त्रुटि", - "NO_GROUPS_FOUND": "कोई समूह नहीं मिला", - "NO_CHATS_FOUND": "कोई चैट नहीं मिला", - "MEDIA_MESSAGE": "मीडिया संदेश", - "INCOMING_AUDIO_CALL": "आवक ऑडियो कॉल", - "INCOMING_VIDEO_CALL": "आने वाली वीडियो कॉल", - "DECLINE": "अस्वीकार", - "ACCEPT": "स्वीकार करें", - "CALL_INITIATED": "कॉल आरंभिक", - "OUTGOING_AUDIO_CALL": "जावक ऑडियो कॉल", - "OUTGOING_VIDEO_CALL": "जावक वीडियो कॉल", - "CALL_REJECTED": "कॉल अस्वीकृत", - "REJECTED_CALL": "अस्वीकृत कॉल", - "CALL_ACCEPTED": "कॉल स्वीकृत", - "JOINED": "शामिल हो गए", - "LEFT_THE_CALL": "कॉल छोड़ दिया", - "UNANSWERED_AUDIO_CALL": "अनुत्तरित ऑडियो कॉल", - "UNANSWERED_VIDEO_CALL": "अनुत्तरित वीडियो कॉल", - "CALL_ENDED": "कॉल समाप्त", - "CANCELLED_CALL": "कैंसिल किया गया कॉल", - "CALL_BUSY": "व्यस्त कॉल करें", - "CALLING": "कॉल कर रहा है...", - "ADD": "जोड़ें", - "NO_BANNED_MEMBERS_FOUND": "कोई प्रतिबंधित सदस्य नहीं मिला", - "BANNED_MEMBERS": "प्रतिबंधित सदस्य", - "NAME": "नाम", - "SCOPE": "स्कोप", - "UNBAN": "Unban", - "SELECT_GROUP_TYPE": "समूह क़िस्म चुनें", - "ENTER_GROUP_PASSWORD": "समूह कूटशब्द भरें", - "CREATE": "बनाएँ", - "CREATE_POLL": "सर्वेक्षण बनाएँ", - "QUESTION": "प्रश्न", - "ENTER_YOUR_QUESTION": "अपना प्रश्न दर्ज करें", - "OPTIONS": "विकल्प", - "ENTER_YOUR_OPTION": "अपना विकल्प दर्ज करें", - "ADD_NEW_OPTION": "नया विकल्प जोड़ें", - "VIEW_MEMBERS": "सदस्य देखें", - "DETAILS": "विवरण", - "NOTIFICATIONS": "सूचनाएँ", - "OTHER": "अन्य", - "HELP": "मदद", - "REPORT_PROBLEM": "किसी समस्या की रिपोर्ट करें", - "GROUP_MEMBERS": "समूह के सदस्य", - "BAN": "बान", - "KICK": "लात", - "PICK_YOUR_EMOJI": "अपने इमोजी उठाओ", - "PRIVATE_GROUP": "निजी समूह", - "PROTECTED_GROUP": "सुरक्षित समूह", - "VISIT": "विज़िट करें", - "ATTACH": "संलग्न करें", - "ATTACH_FILE": "फ़ाइल संलग्न करें", - "ATTACH_VIDEO": "वीडियो संलग्न करें", - "ATTACH_AUDIO": "ऑडियो संलग्न करें", - "ATTACH_IMAGE": "छवि संलग्न करें", - "COLLABORATE_USING_DOCUMENT": "दस्तावेज़ का उपयोग करके सहयोग करें", - "COLLABORATE_USING_WHITEBOARD": "व्हाइटबोर्ड का उपयोग करके सहयोग करें", - "EMOJI": "इमोजी", - "ENTER_YOUR_MESSAGE_HERE": "अपना संदेश यहाँ दर्ज करें", - "NO_MESSAGES_FOUND": "कोई संदेश नहीं मिला", - "THREAD": "धागा", - "COLLABORATIVE_DOCUMENT": "सहयोगी दस्तावेज़", - "COLLABORATIVE_WHITEBOARD": "सहयोगी व्हाइटबोर्ड", - "ADD_REACTION": "प्रतिक्रिया जोड़ें", - "NO_STICKERS_FOUND": "कोई स्टिकर नहीं मिला", - "REPLY_TO_THREAD": "थ्रेड को जवाब दें", - "REPLY_IN_THREAD": "थ्रेड में जवाब दें", - "DELETE_MESSAGE": "संदेश मिटाएँ", - "EDIT_MESSAGE": "संदेश संपादित करें", - "OWNER": "मालिक", - "CHANGE_SCOPE": "स्कोप बदलें", - "STICKER": "स्टीकर", - "LAST_ACTIVE_AT": "पर अंतिम सक्रिय", - "VOICE_CALL": "वॉयस कॉल", - "VIEW_DETAIL": "विवरण देखें", - "VOTES": "वोट", - "VOTE": "वोट", - "NO_VOTE": "कोई वोट नहीं", - "REACTED": "प्रतिक्रिया व्यक्त की", - "ADDED": "जोड़ा गया", - "UNBANNED": "अप्रतिबंधित", - "MADE": "बनाया", - "UNANSWERED_CALL": "अनुत्तरित कॉल", - "MISSED_AUDIO_CALL": "मिस ऑडियो कॉल", - "ENTER_YOUR_PASSWORD": "अपना पासवर्ड दर्ज करें", - "DOCS": "डॉक्स", - "NO_RECORDS_FOUND": "कोई रिकॉर्ड नहीं मिला", - "LIVE_REACTION": "लाइव रिएक्शन", - "SMILEY_PEOPLE": "स्माइली और लोग", - "ANIMALES_NATURE": "पशु और प्रकृति", - "FOOD_DRINK": "खाद्य और पेय", - "ACTIVITY": "गतिविधि", - "TRAVEL_PLACES": "यात्रा और स्थान", - "OBJECTS": "वस्तुएँ", - "SYMBOLS": "प्रतीक", - "FLAGS": "झंडे", - "SENT": "भेजा गया", - "SEEN": "देखा", - "DELIVERED": "डिलीवर", - "TRANSLATE_MESSAGE": "संदेश का अनुवाद करें", - "LEFT": "छोड़ दिया है", - "KICKED": "लात मारी", - "BANNED": "बारित", - "NEW_MESSAGES": "नए संदेश", - "NEW_MESSAGE": "नया संदेश", - "JUMP": "कूदो", - "SELECT_VIDEO_SOURCE": "वीडियो स्रोत चुनें", - "SELECT_INPUT_AUDIO_SOURCE": "इनपुट ऑडियो स्रोत चुनें", - "SELECT_OUTPUT_AUDIO_SOURCE": "आउटपुट ऑडियो स्रोत चुनें", - "INITIATED_GROUP_CALL": "ने एक समूह कॉल शुरू की है", - "YOU_INITIATED_GROUP_CALL": "आपने एक समूह कॉल शुरू की है", - "IGNORE": "ध्यान न दें", - "ON_ANOTHER_CALL": "एक और कॉल पर है", - "CREATING": "बनाना", - "AVATAR": "अवतार", - "GROUP_NAME_BLANK": "समूह का नाम खाली नहीं होना चाहिए", - "GROUP_TYPE_BLANK": "समूह प्रकार कैननॉट खाली हो", - "GROUP_PASSWORD_BLANK": "समूह कूटशब्द खाली नहीं होना चाहिए", - "POLL_QUESTION_BLANK": "प्रश्न कैनॉट खाली हो", - "POLL_OPTION_BLANK": "विकल्प कैननॉट खाली हो", - "ONGOING_CALL": "चल रही कॉल", - "YOU_ALREADY_ONGOING_CALL": "आप पहले से ही चल रहे कॉल में हैं", - "RESIZE": "आकार बदलें", - "SETTINGS": "सेटिंग्स", - "ACTIONS": "क्रियाएँ", - "VIEW_PROFILE": "प्रोफ़ाइल देखें", - "SEND_MESSAGE_IN_PRIVATE": "निजी तौर पर संदेश भेजें", - "DELETE": "मिटाएँ", - "DELETE_CONFIRM": "क्या आप वाकई मिटाना चाहते हैं?", - "CANCEL": "रद्द करें", - "LEAVE_CONFIRM": "क्या आप निश्चित हैं कि आप समूह छोड़ना चाहते हैं?", - "TRANSFER_CONFIRM": "आप समूह स्वामी हैं, कृपया समूह छोड़ने से पहले किसी सदस्य को स्वामित्व स्थानांतरित करें", - "ADDING": "जोड़ रहा है...", - "TRANSFER": "ट्रांसफर", - "TRANSFERRING": "ट्रांसफर", - "YES": "हाँ", - "NO": "नहीं", - "SOMETHING_WRONG": "कुछ गलत हो गया, कृपया फिर से कोशिश करें", - "INVALID_GROUP_NAME": "कृपया समूह के लिए एक वैध नाम दर्ज करें और पुनः प्रयास करें", - "INVALID_GROUP_TYPE": "कृपया समूह के लिए एक वैध प्रकार दर्ज करें और पुनः प्रयास करें", - "INVALID_PASSWORD": "कृपया समूह के लिए एक वैध पासवर्ड दर्ज करें और पुनः प्रयास करें", - "WRONG_PASSWORD": "कृपया सही पासवर्ड दर्ज करें और पुनः प्रयास करें", - "INVALID_POLL_QUESTION": "चुनाव बनाने से पहले कृपया आवश्यक प्रश्न दर्ज करें", - "INVALID_POLL_OPTION": "चुनाव बनाने से पहले कृपया आवश्यक उत्तर दर्ज करें", - "SAME_LANGUAGE_MESSAGE": "अनुवाद के लिए चुनी गई भाषा मूल संदेश की भाषा के समान है", - "LEAVE": "लीव", - "CALLS": "कॉल", - "CUSTOM_MESSAGE_LOCATION": "📍 स्थान", - "OFFLINE": "ऑफ़लाइन", - "YOU": "आप", - "PRIVACY": "निजता", - "BLOCKED_USERS": "अवरोधित उपयोगकर्ता", - "YOU'VE_BLOCKED": "आपने अवरोधित किया है", - "NO_PHOTOS": "कोई तस्वीरें नहीं", - "NO_VIDEOS": "कोई वीडियो नहीं", - "NO_DOCUMENTS": "कोई दस्तावेज़ नहीं", - "JOIN": "शामिल हों", - "DELETE_CONFIRM_MESSAGE": "क्या आप इस बातचीत को मिटाना चाहेंगे? यह बातचीत आपके सभी डिवाइस से हटा दी जाएगी।", - "CHAT_ERROR_MESSAGE": "चैट लोड नहीं किया जा सकता। कृपया फिर से कोशिश करें", - "TRY_AGAIN": "फिर से कोशिश करें", - "CONFIRM": "पुष्टि करें", - "UNSAFE_CONTENT": "असुरक्षित कॉन्टेंट", - "UNSAFE_CONFIRMATION": "क्या आप निश्चित रूप से इस असुरक्षित सामग्री को देखना चाहते हैं", - "AUDIO_FILE": "ऑडियो फ़ाइल", - "SHARED_FILE": "शेयर्ड फ़ाइल", - "OPEN_DOCUMENT": "दस्तावेज़ खोलें", - "OPEN_WHITEBOARD": "व्हाइटबोर्ड खोलें", - "PEOPLE_VOTED": "लोग वोट देते हैं", - "ADD_TO_CHAT": "चैट में जोड़ें", - "TAKE_PHOTO": "फ़ोटो लें", - "SET_THE_ANSWERS": "जवाब सेट करें", - "ADD_ANOTHER_ANSWER": "एक और जवाब जोड़ें", - "ANSWER": "उत्तर", - "IN_A_THREAD": "एक धागे में ⤵", - "ERROR_GROUP_CREATE": "समूह नहीं बना सकता", - "TRY_AGAIN_LATER": "कृपया बाद में फिर से कोशिश करें", - "CONTINUE": "जारी रखें", - "GROUP_NAME_MAX": "समूह के नाम में अधिकतम 25 वर्णों की अनुमति है", - "PASSWORD_MAX": "ग्रुप पासवर्ड में अधिकतम 16 अक्षर की अनुमति है", - "GROUP_PASSWORD": "ग्रुप पासवर्ड", - "INCORRECT_PASSWORD": "गलत कूटशब्द", - "OPEN_WHITEBOARD_TO_DRAW": "एक साथ खींचने के लिए व्हाइटबोर्ड खोलें", - "CALL_HISTORY": "कॉल हिस्ट्री", - "NO_CALL_HISTORY": "अभी तक कोई कॉल नहीं किया गया है", - "CALL_DETAILS": "कॉल का विवरण", - "CONFERENCE_CALL": "कॉन्फ़्रेंस कॉल", - "NEW_GROUP": "न्यू ग्रुप", - "TRANSFER_OWNERSHIP": "स्वामित्व ट्रांसफर करें", - "COPY_MESSAGE": "कॉपी करें", - "SHARE": "शेयर करें", - "OPEN_DOCUMENT_TO_DRAW": "एक साथ आरेखित करने के लिए दस्तावेज़ खोलें", - "INFORMATION": "जानकारी", - "COPY_TEXT": "टेक्स्ट कॉपी करें", - "FORWARD": "फ़ॉरवर्ड", - "TEXT": "टेक्स्ट", - "INCOMING_CALL": "इनकमिंग कॉल", - "OUTGOING_CALL": "आउटगोइंग कॉल", - "MISSED_CALL": "मिस्ड कॉल", - "NEW_CONVERSATION": "नई चैट", - "FORWARDING": "संदेश भेजा जा रहा है..", - "READ": "पढ़ें", - "No_RECIPIENT": "कोई प्राप्तकर्ता नहीं", - "RECEIPT_INFORMATION": "रसीद की जानकारी", - "MESSAGE": "सन्देश", - "GENERATING_SUMMARY":"सारांश तैयार करना", - "CONVERSATION_SUMMARY":"बातचीत का सारांश", - "GENERATE_SUMMARY":"एक सारांश जनरेट करें", - "COMETCHAT_BOT_FIRST_MESSAGE":"मैं इस बातचीत में आपकी कैसे मदद कर सकता हूं? कृपया मुझसे एक प्रश्न पूछें और मैं आपको सलाह दूंगा 🙂", - "COMETCHAT_ASK_BOT":"पूछो", - "COMETCHAT_ASK_AI_BOT":"AI बॉट्स से पूछें", - "COMETCHAT_ASK_BOT_SUBTITLE":"एआई बॉट", - "FORM_COMPLETION_MESSAGE": "फॉर्म भरने के लिए धन्यवाद।", - "FORM":"प्रपत्र", - "CARD":"कार्ड", - "RECORDINGS":"रिकॉर्डिंग्स", - "PARTICIPANTS":"प्रतिभागी", - "NO_PARTICIPANTS":"कोई सहभागी नहीं", - "NO_RECORDINGS":"कोई रिकॉर्डिंग नहीं", - "CALL_LOGS":"कॉल लॉग्स", - "CANCELLED_AUDIO_CALL":"रद्द किया गया ऑडियो कॉल", - "CANCELLED_VIDEO_CALL":"रद्द किया गया वीडियो कॉल", - "MICROPHONE_PERMISSION":"हम ऑडियो संदेशों को रिकॉर्ड करने और साझा करने के लिए माइक्रोफ़ोन का उपयोग करते हैं। माइक्रोफ़ोन ऐक्सेस चालू करने के लिए सेटिंग में जाएँ" -} \ No newline at end of file + "USERS": "यूज़र", + "CHATS": "चैट्स", + "GROUPS": "ग्रुप्स", + "MORE": "ज़्यादा", + "MESSAGE_IMAGE": "📷 छवि", + "MESSAGE_FILE": "📁 फ़ाइल", + "MESSAGE_VIDEO": "📹 वीडियो", + "MESSAGE_AUDIO": "🎵 ऑडियो", + "CUSTOM_MESSAGE": "आपके पास एक संदेश है", + "MISSED_VOICE_CALL": "मिस्ड वॉइस कॉल", + "MISSED_VIDEO_CALL": "मिस्ड वीडियो कॉल", + "CUSTOM_MESSAGE_POLL": "📊 पोल", + "CUSTOM_MESSAGE_STICKER": "💟 स्टिकर", + "CUSTOM_MESSAGE_DOCUMENT": "📃 दस्तावेज़", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 व्हाइटबोर्ड", + "ONLINE": "ऑनलाइन", + "ADMINISTRATOR": "प्रशासक", + "MODERATOR": "प्रस्तुतकर्ता", + "PARTICIPANT": "प्रतिभागी", + "SUGGEST_A_REPLY": "जवाब सुझाएं", + "GENERATIONG_ICEBREAKER": "आइसब्रेकर बनाना", + "PUBLIC": "पब्लिक", + "NO_REPLIES_FOUND": "कोई जवाब नहीं मिला", + "PRIVATE": "निजी", + "PASSWORD_PROTECTED": "पासवर्ड प्रोटेक्टेड", + "PRIVACY_AND_SECURITY": "गोपनीयता और सुरक्षा", + "PREFERENCES": "प्राथमिकताएं", + "MEMBERS": "सदस्य", + "TODAY": "टुडे", + "YESTERDAY": "कल", + "SUNDAY": "रविवार", + "MONDAY": "सोमवार", + "TUESDAY": "मंगलवार", + "WEDNESDAY": "बुधवार", + "THURSDAY": "गुरुवार", + "FRIDAY": "शुक्रवार", + "SATURDAY": "शनिवार", + "TYPING": "टाइप कर रहा है...", + "IS_TYPING": "टाइप कर रहा है...", + "CLOSE": "बंद करें", + "ENTER_GROUP_NAME": "ग्रुप का नाम दर्ज करें", + "ADD_MEMBERS": "सदस्य जोड़ें", + "SEND_MESSAGE": "सन्देश भेजो", + "UNBLOCK_USER": "यूज़र को अनब्लॉक करें", + "BLOCK_USER": "उपयोगकर्ता को ब्लॉक करें", + "DELETE_AND_EXIT": "हटाएँ और बाहर निकलें", + "LEAVE_GROUP": "लीव ग्रुप", + "CREATE_GROUP": "ग्रुप बनाएं", + "SHARED_MEDIA": "शेयर्ड मीडिया", + "VIDEO_CALL": "वीडियो कॉल", + "AUDIO_CALL": "ऑडियो कॉल", + "LOADING": "लोड हो रहा है...", + "REPLY": "उत्तर दें", + "REPLIES": "उत्तर", + "LAUNCH": "लांच", + "SHARED_COLLABORATIVE_DOCUMENT": "ने एक सहयोगी दस्तावेज़ साझा किया है", + "SHARED_COLLABORATIVE_WHITEBOARD": "ने एक सहयोगी व्हाइटबोर्ड साझा किया है", + "CREATED_WHITEBOARD": "आपने एक नया सहयोगी व्हाइटबोर्ड बनाया है", + "CREATED_DOCUMENT": "आपने एक नया SomCollaborative दस्तावेज़ बनाया है", + "PHOTOS": "फ़ोटोज़", + "VIDEOS": "वीडियो", + "DOCUMENT": "दस्तावेज़", + "MESSAGE_IS_DELETED": "संदेश हटा दिया गया है", + "THIS_MESSAGE_DELETED": "⚠️ यह संदेश हटा दिया गया था", + "VIEW_ON_YOUTUBE": "यूट्यूब पर देखें", + "SEARCH": "सर्च करें", + "NO_USERS_FOUND": "कोई यूज़र नहीं मिला", + "ERROR": "एरर", + "NO_GROUPS_FOUND": "कोई समूह नहीं मिला", + "NO_CHATS_FOUND": "कोई चैट नहीं मिली", + "MEDIA_MESSAGE": "मीडिया संदेश", + "INCOMING_AUDIO_CALL": "इनकमिंग ऑडियो कॉल", + "INCOMING_VIDEO_CALL": "इनकमिंग वीडियो कॉल", + "DECLINE": "डिक्लाइन", + "ACCEPT": "स्वीकारें", + "CALL_INITIATED": "कॉल शुरू किया गया", + "OUTGOING_AUDIO_CALL": "आउटगोइंग ऑडियो कॉल", + "OUTGOING_VIDEO_CALL": "आउटगोइंग वीडियो कॉल", + "CALL_REJECTED": "कॉल अस्वीकृत", + "REJECTED_CALL": "रिजेक्टेड कॉल", + "CALL_ACCEPTED": "कॉल स्वीकार किया गया", + "JOINED": "शामिल हो गए", + "LEFT_THE_CALL": "कॉल छोड़ दिया", + "UNANSWERED_AUDIO_CALL": "अनुत्तरित ऑडियो कॉल", + "UNANSWERED_VIDEO_CALL": "अनुत्तरित वीडियो कॉल", + "CALL_ENDED": "कॉल समाप्त हुआ", + "CANCELLED_CALL": "रद्द की गई कॉल", + "CALL_BUSY": "व्यस्त को कॉल करें", + "CALLING": "कॉलिंग...।", + "ADD": "जोड़ें", + "NO_BANNED_MEMBERS_FOUND": "कोई प्रतिबंधित सदस्य नहीं मिला", + "BANNED_MEMBERS": "प्रतिबंधित सदस्य", + "NAME": "नाम", + "SCOPE": "स्कोप", + "UNBAN": "अनबन", + "SELECT_GROUP_TYPE": "ग्रुप क़िस्म चुनें", + "ENTER_GROUP_PASSWORD": "ग्रुप पासवर्ड डालें", + "CREATE": "बनाएँ", + "CREATE_POLL": "पोल बनाएं", + "QUESTION": "सवाल", + "ENTER_YOUR_QUESTION": "अपना प्रश्न दर्ज करें", + "OPTIONS": "ऑप्शन्स", + "ENTER_YOUR_OPTION": "अपना विकल्प दर्ज करें", + "ADD_NEW_OPTION": "नया विकल्प जोड़ें", + "VIEW_MEMBERS": "सदस्यों को देखें", + "DETAILS": "विवरण", + "NOTIFICATIONS": "सूचनाएं", + "OTHER": "अन्य", + "HELP": "मदद", + "REPORT_PROBLEM": "समस्या की रिपोर्ट करें", + "GROUP_MEMBERS": "ग्रुप के सदस्य", + "BAN": "बैन", + "KICK": "किक", + "PICK_YOUR_EMOJI": "अपना इमोजी चुनें", + "PRIVATE_GROUP": "प्राइवेट ग्रुप", + "PROTECTED_GROUP": "संरक्षित समूह", + "VISIT": "विजिट करें", + "ATTACH": "अटैच करें", + "ATTACH_FILE": "फ़ाइल संलग्न करें", + "ATTACH_VIDEO": "वीडियो संलग्न करें", + "ATTACH_AUDIO": "ऑडियो अटैच करें", + "ATTACH_IMAGE": "छवि संलग्न करें", + "COLLABORATIVE_DOCUMENT": "सहयोगात्मक दस्तावेज़", + "COLLABORATIVE_WHITEBOARD": "सहयोगात्मक व्हाइटबोर्ड", + "COLLABORATE_USING_DOCUMENT": "दस्तावेज़ का उपयोग करके सहयोग करें", + "COLLABORATE_USING_WHITEBOARD": "व्हाइटबोर्ड का उपयोग करके सहयोग करें", + "EMOJI": "इमोजी", + "ENTER_YOUR_MESSAGE_HERE": "अपना संदेश यहां दर्ज करें", + "NO_MESSAGES_FOUND": "यहां अभी तक कोई संदेश नहीं है...", + "THREAD": "थ्रेड", + "ADD_REACTION": "प्रतिक्रिया जोड़ें", + "NO_STICKERS_FOUND": "कोई स्टिकर नहीं मिला", + "REPLY_TO_THREAD": "थ्रेड का जवाब दें", + "REPLY_IN_THREAD": "थ्रेड में जवाब दें", + "DELETE_MESSAGE": "संदेश मिटाएँ", + "EDIT_MESSAGE": "संदेश संपादित करें", + "OWNER": "मालिक", + "CHANGE_SCOPE": "स्कोप बदलें", + "STICKER": "स्टिकर", + "LAST_ACTIVE_AT": "पिछली बार सक्रिय", + "VOICE_CALL": "वॉइस कॉल", + "VIEW_DETAIL": "विस्तार से देखें", + "VOTES": "वोट", + "VOTE": "वोट", + "NO_VOTE": "कोई वोट नहीं", + "REACTED": "प्रतिक्रिया व्यक्त की", + "ADDED": "जोड़ा गया", + "UNBANNED": "अप्रतिबंधित", + "MADE": "बनाया", + "UNANSWERED_CALL": "अनुत्तरित कॉल", + "MISSED_AUDIO_CALL": "मिस्ड ऑडियो कॉल", + "ENTER_YOUR_PASSWORD": "अपना पासवर्ड डालें", + "DOCS": "डॉक्स", + "NO_RECORDS_FOUND": "कोई रिकॉर्ड नहीं मिला", + "LIVE_REACTION": "लाइव रिएक्शन", + "SMILEY_PEOPLE": "स्माइलीज एंड पीपल", + "ANIMALES_NATURE": "पशु और प्रकृति", + "FOOD_DRINK": "खाना और पीना", + "ACTIVITY": "गतिविधि", + "TRAVEL_PLACES": "यात्रा और स्थान", + "OBJECTS": "ऑब्जेक्ट्स", + "SYMBOLS": "सिंबल", + "FLAGS": "फ्लैग्स", + "SENT": "भेजा गया", + "SEEN": "देखा गया", + "DELIVERED": "डिलीवर किया", + "TRANSLATE_MESSAGE": "संदेश का अनुवाद करें", + "LEFT": "छोड़ दिया है", + "KICKED": "लात मारी", + "BANNED": "बारित", + "NEW_MESSAGES": "नए संदेश", + "NEW_MESSAGE": "नया संदेश", + "JUMP": "जंप", + "SELECT_VIDEO_SOURCE": "वीडियो स्रोत चुनें", + "SELECT_INPUT_AUDIO_SOURCE": "इनपुट ऑडियो स्रोत का चयन करें", + "SELECT_OUTPUT_AUDIO_SOURCE": "आउटपुट ऑडियो स्रोत चुनें", + "INITIATED_GROUP_CALL": "समूह कॉल शुरू किया है", + "YOU_INITIATED_GROUP_CALL": "आपने ग्रुप कॉल शुरू किया है", + "IGNORE": "ध्यान न दें", + "ON_ANOTHER_CALL": "एक और कॉल पर है", + "CREATING": "बनाना", + "AVATAR": "पुनर्जन्म", + "ONGOING_CALL": "चल रही कॉल", + "YOU_ALREADY_ONGOING_CALL": "आप पहले से चल रही कॉल में हैं", + "RESIZE": "आकार बदलें", + "SETTINGS": "सेटिंग्स", + "ACTIONS": "क्रियाएँ", + "VIEW_PROFILE": "प्रोफ़ाइल देखें", + "SEND_MESSAGE_IN_PRIVATE": "संदेश निजी तौर पर भेजें", + "DELETE": "मिटाएँ", + "DELETE_CONFIRM": "क्या आप वाकई डिलीट करना चाहते हैं?", + "CANCEL": "कैंसिल करें", + "LEAVE_CONFIRM": "क्या आप वाकई समूह छोड़ना चाहते हैं?", + "TRANSFER_CONFIRM": "आप समूह के स्वामी हैं; कृपया समूह छोड़ने से पहले किसी सदस्य को स्वामित्व हस्तांतरित करें", + "ADDING": "जोड़ रहा है...", + "TRANSFER": "ट्रांसफ़र", + "TRANSFERRING": "ट्रांसफर किया जा रहा है", + "YES": "हाँ", + "NO": "नहीं", + "SOMETHING_WRONG": "कुछ ग़लत हुआ; कृपया फिर से कोशिश करें।", + "INVALID_GROUP_NAME": "कृपया ग्रुप के लिए मान्य नाम दर्ज़ करें और फिर से कोशिश करें", + "INVALID_PASSWORD": "कृपया ग्रुप के लिए एक मान्य पासवर्ड डालें और फिर से कोशिश करें", + "INVALID_GROUP_TYPE": "कृपया समूह के लिए एक मान्य प्रकार दर्ज करें और फिर से कोशिश करें", + "WRONG_PASSWORD": "कृपया सही पासवर्ड डालें और फिर से कोशिश करें", + "INVALID_POLL_QUESTION": "पोल बनाने से पहले कृपया आवश्यक प्रश्न दर्ज करें", + "INVALID_POLL_OPTION": "पोल बनाने से पहले कृपया आवश्यक उत्तर दर्ज करें", + "SAME_LANGUAGE_MESSAGE": "अनुवाद के लिए चुनी गई भाषा मूल संदेश की भाषा के समान है", + "LEAVE": "लीव", + "CUSTOM_MESSAGE_LOCATION": "📍 स्थान", + "IN_A_THREAD": "एक सूत्र में ⤵", + "CALLS": "कॉल्स", + "OFFLINE": "ऑफ़लाइन", + "YOU": "आप", + "PRIVACY": "प्राइवेसी", + "BLOCKED_USERS": "ब्लॉक किए गए यूज़र", + "YOU'VE_BLOCKED": "आपने ब्लॉक कर दिया है", + "NO_PHOTOS": "कोई फ़ोटो नहीं", + "NO_VIDEOS": "कोई वीडियो नहीं", + "NO_DOCUMENTS": "कोई दस्तावेज़ नहीं", + "GENERATING_REPLIES": "जवाब जनरेट कर रहा", + "JOIN": "जुड़ें", + "DELETE_CONFIRM_MESSAGE": "क्या आप इस वार्तालाप को मिटाना चाहेंगे? यह वार्तालाप आपके सभी डिवाइस से हटा दिया जाएगा।", + "CHAT_ERROR_MESSAGE": "चैट लोड नहीं हो पा रहे हैं। कृपया फिर से कोशिश करें", + "TRY_AGAIN": "फिर से कोशिश करें", + "CONFIRM": "पुष्टि करें", + "UNSAFE_CONTENT": "असुरक्षित कॉन्टेंट", + "UNSAFE_CONFIRMATION": "क्या आप निश्चित रूप से इस असुरक्षित सामग्री को देखना चाहते हैं", + "AUDIO_FILE": "ऑडियो फ़ाइल", + "SHARED_FILE": "शेयर की गई फ़ाइल", + "OPEN_DOCUMENT": "दस्तावेज़ खोलें", + "OPEN_WHITEBOARD": "व्हाइटबोर्ड खोलें", + "PEOPLE_VOTED": "लोगों ने वोट दिया", + "ADD_TO_CHAT": "चैट में जोड़ें", + "TAKE_PHOTO": "फ़ोटो लें", + "SET_THE_ANSWERS": "उत्तर सेट करें", + "ADD_ANOTHER_ANSWER": "एक और जवाब जोड़ें", + "ANSWER": "उत्तर", + "ERROR_GROUP_CREATE": "समूह नहीं बना सकता", + "TRY_AGAIN_LATER": "कृपया बाद में फिर से कोशिश करें", + "CONTINUE": "जारी रखें", + "GROUP_NAME_MAX": "समूह के नाम में अधिकतम 25 वर्ण अनुमत हैं", + "GROUP_PASSWORD_BLANK": "कृपया कूटशब्द प्रविष्ट करें", + "PASSWORD_MAX": "ग्रुप पासवर्ड में अधिकतम 16 वर्ण अनुमत हैं", + "GROUP_PASSWORD": "ग्रुप पासवर्ड", + "INCORRECT_PASSWORD": "गलत कूटशब्द", + "OPEN_WHITEBOARD_TO_DRAW": "एक साथ खींचने के लिए व्हाइटबोर्ड खोलें", + "CALL_HISTORY": "कॉल हिस्ट्री", + "NO_CALL_HISTORY": "अभी तक कोई कॉल नहीं किया गया है", + "CALL_DETAILS": "कॉल की जानकारी", + "CONFERENCE_CALL": "कॉन्फ़्रेंस कॉल", + "NEW_GROUP": "नया समूह", + "TRANSFER_OWNERSHIP": "ट्रांसफर ओनरशिप", + "COPY_MESSAGE": "कॉपी करें", + "SHARE": "शेयर करें", + "OPEN_DOCUMENT_TO_DRAW": "एक साथ आरेखित करने के लिए दस्तावेज़ खोलें", + "INFORMATION": "संदेश की जानकारी", + "FORWARD": "फॉरवर्ड", + "COPY_TEXT": "टेक्स्ट कॉपी करें", + "TEXT": "टेक्स्ट", + "INCOMING_CALL": "इनकमिंग कॉल", + "OUTGOING_CALL": "आउटगोइंग कॉल", + "MISSED_CALL": "मिस्ड कॉल", + "GROUP_NAME_BLANK": "समूह का नाम खाली नहीं होना चाहिए", + "GROUP_TYPE_BLANK": "समूह का प्रकार खाली नहीं है", + "POLL_QUESTION_BLANK": "प्रश्न: कैनाट खाली है", + "POLL_OPTION_BLANK": "विकल्प खाली नहीं है", + "NEW_CONVERSATION": "नई चैट", + "FORWARDING": "संदेश भेजा जा रहा है...", + "READ": "पढ़ें", + "No_RECIPIENT": "कोई प्राप्तकर्ता नहीं", + "RECEIPT_INFORMATION": "रसीद की जानकारी", + "MESSAGE": "सन्देश", + "GENERATING_SUMMARY": "सारांश तैयार कर रहा है", + "CONVERSATION_SUMMARY": "बातचीत का सारांश", + "GENERATE_SUMMARY": "सारांश जेनरेट करें", + "COMETCHAT_BOT_FIRST_MESSAGE": "मैं इस बातचीत में आपकी मदद कैसे कर सकता हूं? कृपया मुझसे एक प्रश्न पूछें और मैं आपको सलाह दूंगा 🙂", + "COMETCHAT_ASK_BOT": "पूछो", + "COMETCHAT_ASK_AI_BOT": "AI बॉट्स से पूछें", + "COMETCHAT_ASK_BOT_SUBTITLE": "एआई बॉट", + "FORM_COMPLETION_MESSAGE": "फ़ॉर्म भरने के लिए धन्यवाद।", + "FORM": "प्रपत्र", + "CARD": "कार्ड", + "RECORDINGS": "रिकॉर्डिंग्स", + "PARTICIPANTS": "प्रतिभागी", + "NO_PARTICIPANTS": "कोई सहभागी नहीं", + "NO_RECORDINGS": "कोई रिकॉर्डिंग नहीं", + "CALL_LOGS": "कॉल लॉग्स", + "CANCELLED_AUDIO_CALL": "रद्द किया गया ऑडियो कॉल", + "CANCELLED_VIDEO_CALL": "रद्द किया गया वीडियो कॉल", + "MEET_WITH": "से मिलें", + "MIN_MEETING": "न्यूनतम बैठक", + "MORE_TIMES": "ज़्यादा बार", + "SELECT_DAY": "एक दिन चुनें", + "SELECT_TIME": "एक समय चुनें", + "NO_TIME_SLOT_AVAILABLE": "इस तारीख को कोई टाइम स्लॉट उपलब्ध नहीं है। कृपया कोई और तारीख आज़माएँ।", + "BOOK_NEW_SLOT": "नया स्लॉट बुक करें", + "TRY_AGAIN_CAMEL": "फिर से कोशिश करें", + "TIME_SLOT_UNAVAILABLE": "टाइम स्लॉट अब उपलब्ध नहीं है। कृपया कोई दूसरा चुनें।", + "MEETING_SCHEDULER": "मीटिंग शेड्यूलर", + "MIN": "मील" +} diff --git a/src/shared/resources/CometChatLocalize/resources/lt/translation.json b/src/shared/resources/CometChatLocalize/resources/lt/translation.json index dede6b8..ed66154 100644 --- a/src/shared/resources/CometChatLocalize/resources/lt/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/lt/translation.json @@ -1,292 +1,302 @@ { - "USERS": "Vartotojai", - "CHATS": "Susirašinėjimai", - "GROUPS": "Grupės", - "MORE": "Daugiau", - "MESSAGE_IMAGE": "📷 Nuotrauka", - "MESSAGE_FILE": "📁 Failas", - "MESSAGE_VIDEO": "📹 Vaizdo įrašas", - "MESSAGE_AUDIO": "🎵 Garso įrašas", - "CUSTOM_MESSAGE": "Gavote žinutę", - "MISSED_VOICE_CALL": "Praleistas skambutis", - "MISSED_VIDEO_CALL": "Praleistas vaizdo skambutis", - "CUSTOM_MESSAGE_POLL": "📊 Balsavimas", - "CUSTOM_MESSAGE_STICKER": "💟 Lipdukas", - "CUSTOM_MESSAGE_DOCUMENT": "📃 Dokumentas", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 Balta lenta", - "ONLINE": "Prisijungę", - "ADMINISTRATOR": "Administratorius", - "MODERATOR": "Moderatorius", - "NO_REPLIES_FOUND":"Nessuna risposta trovata", - "SUGGEST_A_REPLY":"Suggerisci una risposta", - "GENERATIONG_ICEBREAKER":"Generazione di rompighiaccio", - "PARTICIPANT": "Dalyvis", - "PUBLIC": "Vieša", - "PRIVATE": "Privati", - "PASSWORD_PROTECTED": "Apsaugota slaptažodžiu", - "PRIVACY_AND_SECURITY": "Privatumas ir saugumas", - "PREFERENCES": "Pasirinkimai", - "GENERATING_REPLIES":"Generazione di risposte", - "MEMBERS": "Nariai", - "TODAY": "Šiandien", - "YESTERDAY": "Vakar", - "SUNDAY": "Sekmadienį", - "MONDAY": "Pirmadienį", - "TUESDAY": "Antradienį", - "WEDNESDAY": "Trečiadienį", - "THURSDAY": "Ketvirtadienį", - "FRIDAY": "Penktadienį", - "SATURDAY": "Šeštadienį", - "TYPING": "rašo...", - "IS_TYPING": "dabar rašo...", - "CLOSE": "Uždaryti", - "ENTER_GROUP_NAME": "Įrašykite grupės pavadinimą", - "ADD_MEMBERS": "Pridėti narius", - "SEND_MESSAGE": "Siųsti žinutę", - "UNBLOCK_USER": "Atblokuoti vartotoją", - "BLOCK_USER": "Užblokuoti vartotoją", - "DELETE_AND_EXIT": "Ištrinti ir išeiti", - "LEAVE_GROUP": "Palikti grupę", - "CREATE_GROUP": "Sukurti grupę", - "SHARED_MEDIA": "Pasidalinti failai", - "VIDEO_CALL": "Vaizdo skambutis", - "AUDIO_CALL": "Skambutis", - "LOADING": "Kraunasi...", - "REPLY": "atsakyti", - "REPLIES": "atsakymai", - "LAUNCH": "Įjungti", - "SHARED_COLLABORATIVE_DOCUMENT": "pasidalino bendru dokumentu", - "SHARED_COLLABORATIVE_WHITEBOARD": "pasidalino bendra balta lenta", - "CREATED_WHITEBOARD": "Jūs sukūrėte naują baltą lentą", - "CREATED_DOCUMENT": "Jūs sukūrėte naują bendrą dokumentą", - "PHOTOS": "Nuotraukos", - "VIDEOS": "Vaizdo įrašai", - "DOCUMENT": "Dokumentas", - "YOU_DELETED_THIS_MESSAGE": "⚠️ Jūs ištrynėte žinutę", - "THIS_MESSAGE_DELETED": "⚠️ Ši žinutė buvo ištrinta", - "VIEW_ON_YOUTUBE": "Žiūrėti per Youtube", - "SEARCH": "Ieškoti", - "NO_USERS_FOUND": "Nerasta vartotojų", - "ERROR": "Klaida", - "NO_GROUPS_FOUND": "Nerasta grupių", - "NO_CHATS_FOUND": "Nerasta susirašinėjimų", - "MEDIA_MESSAGE": "Media žinutė", - "INCOMING_AUDIO_CALL": "Skambina", - "INCOMING_VIDEO_CALL": "Skambina su vaizdu", - "DECLINE": "Atmesti", - "ACCEPT": "Pakelti", - "CALL_INITIATED": "Skambutis pradėtas", - "OUTGOING_AUDIO_CALL": "Skambinama", - "OUTGOING_VIDEO_CALL": "Skambinama su vaizdu", - "CALL_REJECTED": "Skambutis atmestas", - "REJECTED_CALL": "atmestas skambutis", - "CALL_ACCEPTED": "Skambutis priimtas", - "JOINED": "prisijungė", - "LEFT_THE_CALL": "paliko skambutį", - "UNANSWERED_AUDIO_CALL": "Neatsakė į skambutį", - "UNANSWERED_VIDEO_CALL": "Neatsakė į vaizdo skambutį", - "CALL_ENDED": "Skambutis baigtas", - "CANCELLED_CALL": "Chiamata annullata", - "CALL_BUSY": "Užimta", - "CALLING": "Skambinama...", - "ADD": "Pridėti", - "NO_BANNED_MEMBERS_FOUND": "Nėra užblokuotų narių", - "BANNED_MEMBERS": "Užblokuoti nariai", - "NAME": "Vardas", - "SCOPE": "Sritis", - "UNBAN": "Atblokuoti", - "SELECT_GROUP_TYPE": "Pasirinkti grupės tipą", - "ENTER_GROUP_PASSWORD": "Įrašykite grupės slaptažodį", - "CREATE": "Sukurti", - "CREATE_POLL": "Sukurti balsavimą", - "QUESTION": "Klausimas", - "ENTER_YOUR_QUESTION": "Įrašykite klausimą", - "OPTIONS": "Pasirinkimai", - "ENTER_YOUR_OPTION": "Įrašykite pasirinkimą", - "ADD_NEW_OPTION": "Pridėti naują pasirinkimą", - "VIEW_MEMBERS": "Žiūrėti narius", - "DETAILS": "Detalės", - "NOTIFICATIONS": "Pranešimai", - "OTHER": "Kiti", - "HELP": "Pagalba", - "REPORT_PROBLEM": "Pranešti apie problemą", - "GROUP_MEMBERS": "Grupės nariai", - "BAN": "Užblokuoti", - "KICK": "Išmesti", - "PICK_YOUR_EMOJI": "Pasirinkite veiduką", - "PRIVATE_GROUP": "Privati grupė", - "PROTECTED_GROUP": "Apsaugota grupė", - "VISIT": "Žiūrėti", - "ATTACH": "Pridėti", - "ATTACH_FILE": "Pridėti failą", - "ATTACH_VIDEO": "Pridėti vaizdo įrašą", - "ATTACH_AUDIO": "Pridėti garso įrašą", - "ATTACH_IMAGE": "Pridėti nuotrauką", - "COLLABORATE_USING_DOCUMENT": "Bendradarbiauti dokumentu", - "COLLABORATE_USING_WHITEBOARD": "Bendradarbiauti balta lenta", - "EMOJI": "Veidukas", - "ENTER_YOUR_MESSAGE_HERE": "Įrašykite žinutę", - "NO_MESSAGES_FOUND": "Nerasta žinučių", - "THREAD": "Tema", - "COLLABORATIVE_DOCUMENT": "Bendras dokumentas", - "COLLABORATIVE_WHITEBOARD": "Bendra balta lenta", - "ADD_REACTION": "Pridėti reakciją", - "NO_STICKERS_FOUND": "Nerasta lipdukų", - "REPLY_TO_THREAD": "Atsakyti į temą", - "REPLY_IN_THREAD": "Atsakyti temoje", - "DELETE_MESSAGE": "Ištrinti žinutę", - "EDIT_MESSAGE": "Redaguoti žinutę", - "OWNER": "Savininkas", - "CHANGE_SCOPE": "Pakeisti sritį", - "STICKER": "Lipdukas", - "LAST_ACTIVE_AT": "Buvo aktyvus", - "VOICE_CALL": "Garso skambutis", - "VIEW_DETAIL": "Žiūrėti detales", - "VOTES": "balsai", - "VOTE": "balsas", - "NO_VOTE": "Nėra balsų", - "REACTED": "sureagavo", - "ADDED": "pridėjo", - "UNBANNED": "atblokavo", - "MADE": "sukūrė", - "UNANSWERED_CALL": "Chiamata senza risposta", - "MISSED_AUDIO_CALL": "Praleistas skambutis", - "ENTER_YOUR_PASSWORD": "Įrašykite slaptažodį", - "DOCS": "Dokumentai", - "NO_RECORDS_FOUND": "Nerasta įrašų", - "LIVE_REACTION": "Gyva reakcija", - "SMILEY_PEOPLE": "Veidukai ir žmonės", - "ANIMALES_NATURE": "Gyvūnai ir gamta", - "FOOD_DRINK": "Maistas ir gėrimai", - "ACTIVITY": "Veikla", - "TRAVEL_PLACES": "Kelionės ir vietos", - "OBJECTS": "Objektai", - "SYMBOLS": "Simboliai", - "FLAGS": "Vėliavos", - "SENT": "Išsiuntė", - "SEEN": "Matė", - "DELIVERED": "Pristatyta", - "TRANSLATE_MESSAGE": "Išversti žinutę", - "LEFT": "paliko", - "KICKED": "buvo išspirtas", - "BANNED": "buvo užbanintas", - "NEW_MESSAGES": "naujos žinutės", - "NEW_MESSAGE": "nauja žinutė", - "JUMP": "Pašokti", - "SELECT_VIDEO_SOURCE": "Pasirinkite vaizdo šaltinį", - "SELECT_INPUT_AUDIO_SOURCE": "Pasirinkite įrašomo garso šaltinį", - "SELECT_OUTPUT_AUDIO_SOURCE": "Pasirinkite siunčiamos garso šaltinį", - "INITIATED_GROUP_CALL": "pradėjo grupės skambutį", - "YOU_INITIATED_GROUP_CALL": "Jūs pradėjote grupės skambutį", - "IGNORE": "Ignoruoti", - "ON_ANOTHER_CALL": "jau yra pokalbyje", - "CREATING": "Kuriame", - "AVATAR": "Avataras", - "GROUP_NAME_BLANK": "Grupės pavadinimas negali būti tuščias", - "GROUP_TYPE_BLANK": "Grupės tipas negali būti tuščias", - "GROUP_PASSWORD_BLANK": "Grupės slaptažodis negali būti tuščias", - "POLL_QUESTION_BLANK": "Klausimas negali būti tuščias", - "POLL_OPTION_BLANK": "Pasirinkimas negali būti tuščias", - "ONGOING_CALL": "Vykdomas skambutis", - "YOU_ALREADY_ONGOING_CALL": "Jūs jau esate vykstančiame skambutyje", - "RESIZE": "Pakeisti dydį", - "SETTINGS": "Nustatymai", - "ACTIONS": "veiksmai", - "VIEW_PROFILE": "Peržiūrėti profilį", - "SEND_MESSAGE_IN_PRIVATE": "Siųsti žinutę privačiai", - "DELETE": "Ištrinti", - "DELETE_CONFIRM": "Ar tikrai norite ištrinti?", - "CANCEL": "Atšaukti", - "LEAVE_CONFIRM": "Ar tikrai norite palikti grupę?", - "TRANSFER_CONFIRM": "Jūs esate grupės savininkas, prašome perduoti nuosavybės teisę nariui prieš išvykstant iš grupės", - "ADDING": "Pridedant...", - "TRANSFER": "Perdavimas", - "TRANSFERRING": "perkėlimas", - "YES": "Taip", - "NO": "Ne", - "SOMETHING_WRONG": "Kažkas nutiko, bandykite dar kartą", - "INVALID_GROUP_NAME": "Įveskite galiojantį grupės pavadinimą ir bandykite dar kartą", - "INVALID_GROUP_TYPE": "Įveskite galiojantį grupės tipą ir bandykite dar kartą", - "INVALID_PASSWORD": "Įveskite galiojantį grupės slaptažodį ir bandykite dar kartą", - "WRONG_PASSWORD": "Įveskite teisingą slaptažodį ir bandykite dar kartą", - "INVALID_POLL_QUESTION": "Prašome įvesti reikiamą klausimą prieš kurdami apklausą", - "INVALID_POLL_OPTION": "Prašome įvesti reikiamą atsakymą prieš kurdami apklausą", - "SAME_LANGUAGE_MESSAGE": "Pasirinkta vertimo kalba yra panaši į originalaus pranešimo kalbą", - "LEAVE": "Palikti", - "CALLS": "Skambučiai", - "CUSTOM_MESSAGE_LOCATION": "📍 Vieta", - "OFFLINE": "Atsijungęs", - "YOU": "Jūs", - "PRIVACY": "Privatumas", - "BLOCKED_USERS": "Užblokuoti nariai", - "YOU'VE_BLOCKED": "Jūs užblokavote", - "NO_PHOTOS": "Nėra nuotraukų", - "NO_VIDEOS": "Nėra vaizdo įrašų", - "NO_DOCUMENTS": "Nėra dokumentų", - "JOIN": "Prisijungti", - "DELETE_CONFIRM_MESSAGE": "Ar norėtumėte ištrinti šį pokalbį? Šis pokalbis bus ištrintas iš visų jūsų įrenginių.", - "CHAT_ERROR_MESSAGE": "Negaliu įkelti pokalbių. Bandykite dar kartą", - "TRY_AGAIN": "BANDYKITE DAR KARTĄ", - "CONFIRM": "Patvirtinti", - "UNSAFE_CONTENT": "NESAUGUS TURINYS", - "UNSAFE_CONFIRMATION": "AR TIKRAI NORITE PAMATYTI ŠĮ NESAUGŲ TURINĮ", - "AUDIO_FILE": "GARSO FAILAS", - "SHARED_FILE": "BENDRASIS FAILAS", - "OPEN_DOCUMENT": "ATVIRAS DOKUMENTAS", - "OPEN_WHITEBOARD": "ATVIRAS BALTASIS ŠERNAS", - "PEOPLE_VOTED": "ŽMONĖS BALSUOJA", - "ADD_TO_CHAT": "PRIDĖTI PRIE CHA", - "TAKE_PHOTO": "FOTOGRAFUOKITE", - "SET_THE_ANSWERS": "NUSTATYKITE ATSAKYMUS", - "ADD_ANOTHER_ANSWER": "PRIDĖTI DAR VIENĄ ATSAKYMĄ", - "ANSWER": "ATSAKYMAS", - "IN_A_THREAD": "In un thread ⤵", - "ERROR_GROUP_CREATE": "Impossibile creare un gruppo", - "TRY_AGAIN_LATER": "Riprova più tardi", - "CONTINUE": "Continua", - "GROUP_NAME_MAX": "Massimo 25 caratteri consentiti nel nome del gruppo", - "PASSWORD_MAX": "Massimo 16 caratteri consentiti nella password del gruppo", - "GROUP_PASSWORD": "Password di gruppo", - "INCORRECT_PASSWORD": "Password errata", - "OPEN_WHITEBOARD_TO_DRAW": "Apri la lavagna per disegnare insieme", - "CALL_HISTORY": "Cronologia chiamate", - "NO_CALL_HISTORY": "Non è stata ancora effettuata alcuna chiamata", - "CALL_DETAILS": "Dettagli della chiamata", - "CONFERENCE_CALL": "teleconferenza", - "NEW_GROUP": "Nuovo gruppo", - "TRANSFER_OWNERSHIP": "Trasferisci la proprietà", - "COPY_MESSAGE": "Copia", - "SHARE": "Condividi", - "OPEN_DOCUMENT_TO_DRAW": "Apri il documento per disegnare insieme", - "INFORMATION": "Informazioni", - "COPY_TEXT": "Copia testo", - "FORWARD": "Avanti", - "TEXT": "Testo", - "INCOMING_CALL": "Chiamata in arrivo", - "OUTGOING_CALL": "Chiamata in uscita", - "MISSED_CALL": "Chiamata persa", - "NEW_CONVERSATION": "Nuova chat", - "FORWARDING": "Invio di messaggi..", - "READ": "Leggi", - "No_RECIPIENT": "Nessun destinatario", - "RECEIPT_INFORMATION": "Informazioni sulla ricevuta", - "MESSAGE": "Messaggio", - "GENERATING_SUMMARY":"Generazione del riassunto", - "CONVERSATION_SUMMARY":"Riepilogo della conversazione", - "GENERATE_SUMMARY":"Genera un riepilogo", - "COMETCHAT_BOT_FIRST_MESSAGE":"Come posso aiutarti con questa conversazione? Per favore fammi una domanda e ti consiglierò 🙂", - "COMETCHAT_ASK_BOT":"Chiedere", - "COMETCHAT_ASK_AI_BOT":"Chiedi ai bot AI", - "COMETCHAT_ASK_BOT_SUBTITLE":"Bot AI", - "FORM_COMPLETION_MESSAGE": "Grazie per aver compilato il modulo.", - "FORM":"Modulo", - "CARD":"Carta", - "RECORDINGS":"Registrazioni", - "PARTICIPANTS":"Partecipanti", - "NO_PARTICIPANTS":"Nessun partecipante", - "NO_RECORDINGS":"Nessuna registrazione", - "CALL_LOGS":"Registri delle chiamate", - "CANCELLED_AUDIO_CALL":"Chiamata audio annullata", - "CANCELLED_VIDEO_CALL":"Videochiamata annullata", - "MICROPHONE_PERMISSION":"Utilizziamo il microfono per registrare e condividere messaggi audio. Vai alle impostazioni per abilitare l'accesso al microfono" -} \ No newline at end of file + "USERS": "Vartotojai", + "CHATS": "Pokalbiai", + "GROUPS": "Grupės", + "MORE": "Daugiau", + "MESSAGE_IMAGE": "📷 Vaizdas", + "MESSAGE_FILE": "📁 Failas", + "MESSAGE_VIDEO": "📹 Vaizdo įrašas", + "MESSAGE_AUDIO": "🎵 Garsas", + "CUSTOM_MESSAGE": "Jūs turite pranešimą", + "MISSED_VOICE_CALL": "Praleistas balso skambutis", + "MISSED_VIDEO_CALL": "Praleistas vaizdo skambutis", + "CUSTOM_MESSAGE_POLL": "📊 Apklausa", + "CUSTOM_MESSAGE_STICKER": "💟 Lipdukas", + "CUSTOM_MESSAGE_DOCUMENT": "📃 Dokumentas", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 Balta lenta", + "ONLINE": "Prisijungę", + "ADMINISTRATOR": "Administratorius", + "MODERATOR": "vedėjas", + "PARTICIPANT": "Dalyvis", + "SUGGEST_A_REPLY": "Pasiūlykite atsakymą", + "GENERATIONG_ICEBREAKER": "Ledlaužių generavimas", + "PUBLIC": "Visuomeninis", + "NO_REPLIES_FOUND": "Atsakymų nerasta", + "PRIVATE": "Privatus", + "PASSWORD_PROTECTED": "Apsaugotas slaptažodžiu", + "PRIVACY_AND_SECURITY": "Privatumas ir saugumas", + "PREFERENCES": "Nuostatos", + "MEMBERS": "Nariai", + "TODAY": "Šiandien", + "YESTERDAY": "Vakar", + "SUNDAY": "Sekmadienis", + "MONDAY": "Pirmadienis", + "TUESDAY": "Antradienis", + "WEDNESDAY": "Trečiadienis", + "THURSDAY": "Ketvirtadienis", + "FRIDAY": "Penktadienis", + "SATURDAY": "Šeštadienis", + "TYPING": "spausdinimas...", + "IS_TYPING": "įrašo...", + "CLOSE": "Uždaryti", + "ENTER_GROUP_NAME": "Įveskite grupės pavadinimą", + "ADD_MEMBERS": "Pridėti narius", + "SEND_MESSAGE": "Siųsti žinutę", + "UNBLOCK_USER": "Atblokuoti vartotoją", + "BLOCK_USER": "Blokuoti vartotoją", + "DELETE_AND_EXIT": "Ištrinti ir išeiti", + "LEAVE_GROUP": "Palikite grupę", + "CREATE_GROUP": "Sukurti grupę", + "SHARED_MEDIA": "Bendroji žiniasklaida", + "VIDEO_CALL": "Vaizdo skambutis", + "AUDIO_CALL": "Garso skambutis", + "LOADING": "Įkeliama...", + "REPLY": "atsakymas", + "REPLIES": "atsakymai", + "LAUNCH": "Paleidimas", + "SHARED_COLLABORATIVE_DOCUMENT": "pasidalino bendradarbiavimu grindžiamu dokumentu", + "SHARED_COLLABORATIVE_WHITEBOARD": "pasidalijo bendradarbiaujančia lenta", + "CREATED_WHITEBOARD": "Sukūrėte naują bendradarbiavimo lentą", + "CREATED_DOCUMENT": "Sukūrėte naują SomCollaborative dokumentą", + "PHOTOS": "Nuotraukos", + "VIDEOS": "Vaizdo įrašai", + "DOCUMENT": "Dokumentas", + "MESSAGE_IS_DELETED": "Pranešimas ištrintas", + "THIS_MESSAGE_DELETED": "⚠️ Šis pranešimas buvo ištrintas", + "VIEW_ON_YOUTUBE": "Žiūrėti “Youtube”", + "SEARCH": "Paieška", + "NO_USERS_FOUND": "Vartotojų nerasta", + "ERROR": "Klaida", + "NO_GROUPS_FOUND": "Grupės nerasta", + "NO_CHATS_FOUND": "Nerasta pokalbių", + "MEDIA_MESSAGE": "Žiniasklaidos pranešimas", + "INCOMING_AUDIO_CALL": "Įeinantis garso skambutis", + "INCOMING_VIDEO_CALL": "Įeinantis vaizdo skambutis", + "DECLINE": "Sumažėjimas", + "ACCEPT": "Priimti", + "CALL_INITIATED": "Skambutis inicijuotas", + "OUTGOING_AUDIO_CALL": "Išeinantis garso skambutis", + "OUTGOING_VIDEO_CALL": "Išeinantis vaizdo skambutis", + "CALL_REJECTED": "Skambutis atmestas", + "REJECTED_CALL": "Atmestas skambutis", + "CALL_ACCEPTED": "Skambutis priimtas", + "JOINED": "prisijungė", + "LEFT_THE_CALL": "paliko skambutį", + "UNANSWERED_AUDIO_CALL": "Neatsakytas garso skambutis", + "UNANSWERED_VIDEO_CALL": "Neatsakytas vaizdo skambutis", + "CALL_ENDED": "Skambutis baigėsi", + "CANCELLED_CALL": "Atšauktas skambutis", + "CALL_BUSY": "Skambinkite užimtas", + "CALLING": "Skambinimas...", + "ADD": "Pridėti", + "NO_BANNED_MEMBERS_FOUND": "Nerasta uždraustų narių", + "BANNED_MEMBERS": "Uždrausti nariai", + "NAME": "Pavadinimas", + "SCOPE": "Taikymo sritis", + "UNBAN": "Unban", + "SELECT_GROUP_TYPE": "Pasirinkite grupės tipą", + "ENTER_GROUP_PASSWORD": "Įveskite grupės slaptažodį", + "CREATE": "Sukurti", + "CREATE_POLL": "Sukurti apklausą", + "QUESTION": "Klausimas", + "ENTER_YOUR_QUESTION": "Įveskite savo klausimą", + "OPTIONS": "Parinktys", + "ENTER_YOUR_OPTION": "Įveskite savo parinktį", + "ADD_NEW_OPTION": "Pridėti naują parinktį", + "VIEW_MEMBERS": "Peržiūrėti narius", + "DETAILS": "Išsami informacija", + "NOTIFICATIONS": "Pranešimai", + "OTHER": "Kita", + "HELP": "Pagalba", + "REPORT_PROBLEM": "Pranešti apie problemą", + "GROUP_MEMBERS": "Grupės nariai", + "BAN": "Uždrausti", + "KICK": "Kick", + "PICK_YOUR_EMOJI": "Pasirinkite jaustuką", + "PRIVATE_GROUP": "Privati grupė", + "PROTECTED_GROUP": "Saugoma grupė", + "VISIT": "Apsilankykite", + "ATTACH": "Pridėti", + "ATTACH_FILE": "Pridėti failą", + "ATTACH_VIDEO": "Pridėti vaizdo įrašą", + "ATTACH_AUDIO": "Pridėkite garso įrašą", + "ATTACH_IMAGE": "Pridėti paveikslėlį", + "COLLABORATIVE_DOCUMENT": "Bendradarbiavimo dokumentas", + "COLLABORATIVE_WHITEBOARD": "Bendradarbiavimo lenta", + "COLLABORATE_USING_DOCUMENT": "Bendradarbiavimas naudojant dokumentą", + "COLLABORATE_USING_WHITEBOARD": "Bendradarbiaukite naudodami lentą", + "EMOJI": "Jaustukai", + "ENTER_YOUR_MESSAGE_HERE": "Įveskite savo pranešimą čia", + "NO_MESSAGES_FOUND": "Žinučių čia dar nėra...", + "THREAD": "Siūlai", + "ADD_REACTION": "Pridėti reakciją", + "NO_STICKERS_FOUND": "Lipdukų nerasta", + "REPLY_TO_THREAD": "Atsakyti į siūlą", + "REPLY_IN_THREAD": "Atsakyti temoje", + "DELETE_MESSAGE": "Ištrinti pranešimą", + "EDIT_MESSAGE": "Redaguoti pranešimą", + "OWNER": "Savininkas", + "CHANGE_SCOPE": "Pakeisti taikymo sritį", + "STICKER": "Lipdukas", + "LAST_ACTIVE_AT": "Paskutinį kartą aktyvus", + "VOICE_CALL": "Balso skambutis", + "VIEW_DETAIL": "Peržiūrėti išsamią informaciją", + "VOTES": "balsų", + "VOTE": "balsas", + "NO_VOTE": "Nėra balsavimo", + "REACTED": "sureagavo", + "ADDED": "pridėta", + "UNBANNED": "neuždraustas", + "MADE": "pagamintas", + "UNANSWERED_CALL": "Neatsakytas skambutis", + "MISSED_AUDIO_CALL": "Praleistas garso skambutis", + "ENTER_YOUR_PASSWORD": "Įveskite slaptažodį", + "DOCS": "Dokumentai", + "NO_RECORDS_FOUND": "Įrašų nerasta", + "LIVE_REACTION": "Gyva reakcija", + "SMILEY_PEOPLE": "Šypsenėlės ir žmonės", + "ANIMALES_NATURE": "Gyvūnai ir gamta", + "FOOD_DRINK": "Maistas ir gėrimai", + "ACTIVITY": "Veikla", + "TRAVEL_PLACES": "Kelionės ir vietos", + "OBJECTS": "Objektai", + "SYMBOLS": "Simboliai", + "FLAGS": "Vėliavos", + "SENT": "Išsiųstas", + "SEEN": "Matyta", + "DELIVERED": "Pristatyta", + "TRANSLATE_MESSAGE": "Išversti pranešimą", + "LEFT": "liko", + "KICKED": "spardė", + "BANNED": "uždraustas", + "NEW_MESSAGES": "Naujos žinutės", + "NEW_MESSAGE": "nauja žinutė", + "JUMP": "Šuolis", + "SELECT_VIDEO_SOURCE": "Pasirinkite vaizdo šaltinį", + "SELECT_INPUT_AUDIO_SOURCE": "Pasirinkite įvesties garso šaltinį", + "SELECT_OUTPUT_AUDIO_SOURCE": "Pasirinkite išvesties garso šaltinį", + "INITIATED_GROUP_CALL": "inicijavo grupinį kvietimą", + "YOU_INITIATED_GROUP_CALL": "Iniciavote grupinį skambutį", + "IGNORE": "Ignoruoti", + "ON_ANOTHER_CALL": "Yra dar vienas skambutis", + "CREATING": "Kurti", + "AVATAR": "Avataras", + "ONGOING_CALL": "Vykstantis skambutis", + "YOU_ALREADY_ONGOING_CALL": "Jūs jau esate nuolatiniame skambutyje", + "RESIZE": "Pakeiskite dydį", + "SETTINGS": "Nustatymai", + "ACTIONS": "Veiksmai", + "VIEW_PROFILE": "Peržiūrėti profilį", + "SEND_MESSAGE_IN_PRIVATE": "Siųsti žinutę privačiai", + "DELETE": "Ištrinti", + "DELETE_CONFIRM": "Ar tikrai norite ištrinti?", + "CANCEL": "Atšaukti", + "LEAVE_CONFIRM": "Ar tikrai norite palikti grupę?", + "TRANSFER_CONFIRM": "Jūs esate grupės savininkas; prašome perduoti nuosavybės teisę nariui prieš išeinant iš grupės", + "ADDING": "Pridedant...", + "TRANSFER": "Pervedimas", + "TRANSFERRING": "Perkėlimas", + "YES": "Taip", + "NO": "Ne", + "SOMETHING_WRONG": "Kažkas nutiko, pabandykite dar kartą.", + "INVALID_GROUP_NAME": "Įveskite galiojantį grupės pavadinimą ir bandykite dar kartą", + "INVALID_PASSWORD": "Įveskite galiojantį grupės slaptažodį ir bandykite dar kartą", + "INVALID_GROUP_TYPE": "Įveskite galiojantį grupės tipą ir bandykite dar kartą", + "WRONG_PASSWORD": "Įveskite teisingą slaptažodį ir bandykite dar kartą", + "INVALID_POLL_QUESTION": "Prieš kurdami apklausą, įveskite reikiamą klausimą", + "INVALID_POLL_OPTION": "Prieš kurdami apklausą, įveskite reikiamą atsakymą", + "SAME_LANGUAGE_MESSAGE": "Pasirinkta vertimo kalba yra panaši į originalaus pranešimo kalbą", + "LEAVE": "Palikite", + "CUSTOM_MESSAGE_LOCATION": "📍 Vieta", + "IN_A_THREAD": "Siūlyje ⤵", + "CALLS": "Skambučiai", + "OFFLINE": "Neprisijungęs", + "YOU": "Jūs", + "PRIVACY": "Privatumas", + "BLOCKED_USERS": "Užblokuoti vartotojai", + "YOU'VE_BLOCKED": "Jūs užblokavote", + "NO_PHOTOS": "Nuotraukų nėra", + "NO_VIDEOS": "Nėra vaizdo įrašų", + "NO_DOCUMENTS": "Jokių dokumentų", + "GENERATING_REPLIES": "Atsakymų generavimas", + "JOIN": "Prisijungti", + "DELETE_CONFIRM_MESSAGE": "Ar norėtumėte ištrinti šį pokalbį? Šis pokalbis bus ištrintas iš visų jūsų įrenginių.", + "CHAT_ERROR_MESSAGE": "Negalima įkelti pokalbių. Prašome pabandyti dar kartą", + "TRY_AGAIN": "BANDYKITE DAR KARTĄ", + "CONFIRM": "Patvirtinti", + "UNSAFE_CONTENT": "Nesaugus turinys", + "UNSAFE_CONFIRMATION": "Ar tikrai norite pamatyti šį nesaugų turinį", + "AUDIO_FILE": "Garso failas", + "SHARED_FILE": "Bendrinamas failas", + "OPEN_DOCUMENT": "ATIDARYTI DOKUMENTĄ", + "OPEN_WHITEBOARD": "ATVIRA LENTA", + "PEOPLE_VOTED": "Žmonės balsavo", + "ADD_TO_CHAT": "Pridėti prie pokalbio", + "TAKE_PHOTO": "Fotografuokite", + "SET_THE_ANSWERS": "NUSTATYKITE ATSAKYMUS", + "ADD_ANOTHER_ANSWER": "Pridėti kitą atsakymą", + "ANSWER": "Atsakymas", + "ERROR_GROUP_CREATE": "Negalima sukurti grupės", + "TRY_AGAIN_LATER": "Pabandykite dar kartą vėliau", + "CONTINUE": "Tęsti", + "GROUP_NAME_MAX": "Grupės pavadinime leidžiama naudoti ne daugiau kaip 25 kadenciją", + "GROUP_PASSWORD_BLANK": "Įveskite slaptažodį", + "PASSWORD_MAX": "Grupės slaptažodyje leidžiama naudoti ne daugiau kaip 16 adresatų", + "GROUP_PASSWORD": "Grupės slaptažodis", + "INCORRECT_PASSWORD": "Neteisingas slaptažodis", + "OPEN_WHITEBOARD_TO_DRAW": "Atidarykite lentą, kad galėtumėte piešti kartu", + "CALL_HISTORY": "Skambučių istorija", + "NO_CALL_HISTORY": "Kol kas nebuvo skambinta", + "CALL_DETAILS": "Išsami informacija apie skambutį", + "CONFERENCE_CALL": "konferencinis skambutis", + "NEW_GROUP": "Nauja grupė", + "TRANSFER_OWNERSHIP": "Nuosavybės perkėlimas", + "COPY_MESSAGE": "Kopijuoti", + "SHARE": "Dalintis", + "OPEN_DOCUMENT_TO_DRAW": "Atidarykite dokumentą, kad galėtumėte piešti kartu", + "INFORMATION": "Informacija apie pranešimą", + "FORWARD": "Persiųsti", + "COPY_TEXT": "Kopijuoti tekstą", + "TEXT": "Tekstas", + "INCOMING_CALL": "Įeinantis skambutis", + "OUTGOING_CALL": "Išeinantis skambutis", + "MISSED_CALL": "Praleistas skambutis", + "GROUP_NAME_BLANK": "Grupės pavadinimas neturėtų būti tuščias", + "GROUP_TYPE_BLANK": "Grupės tipas Negalima yra tuščias", + "POLL_QUESTION_BLANK": "Klausimas “canaut” yra tuščias", + "POLL_OPTION_BLANK": "Parinktis negalima yra tuščia", + "NEW_CONVERSATION": "Naujas pokalbis", + "FORWARDING": "Žinučių siuntimas...", + "READ": "Skaityti", + "No_RECIPIENT": "Nėra gavėjo", + "RECEIPT_INFORMATION": "Informacija apie kvitą", + "MESSAGE": "Pranešimas", + "GENERATING_SUMMARY": "Santraukos generavimas", + "CONVERSATION_SUMMARY": "Pokalbio santrauka", + "GENERATE_SUMMARY": "Sukurkite santrauką", + "COMETCHAT_BOT_FIRST_MESSAGE": "Kaip galėčiau jums padėti šiame pokalbyje? Užduokite man klausimą ir aš jums patarsiu 🙂", + "COMETCHAT_ASK_BOT": "Klauskite", + "COMETCHAT_ASK_AI_BOT": "Paklauskite AI robotų", + "COMETCHAT_ASK_BOT_SUBTITLE": "AI robotas", + "FORM_COMPLETION_MESSAGE": "Dėkojame, kad užpildėte formą.", + "FORM": "Forma", + "CARD": "Kortelė", + "RECORDINGS": "Įrašai", + "PARTICIPANTS": "Dalyviai", + "NO_PARTICIPANTS": "Dalyvių nėra", + "NO_RECORDINGS": "Nėra įrašų", + "CALL_LOGS": "Skambučių žurnalai", + "CANCELLED_AUDIO_CALL": "Atšauktas garso skambutis", + "CANCELLED_VIDEO_CALL": "Atšauktas vaizdo skambutis", + "MEET_WITH": "Susipažinkite su", + "MIN_MEETING": "min susitikimas", + "MORE_TIMES": "Daugiau kartų", + "SELECT_DAY": "Pasirinkite dieną", + "SELECT_TIME": "Pasirinkite laiką", + "NO_TIME_SLOT_AVAILABLE": "Šią dieną nėra laiko tarpų. Pabandykite kitą datą.", + "BOOK_NEW_SLOT": "Užsisakykite naują lizdą", + "TRY_AGAIN_CAMEL": "Pabandykite dar kartą", + "TIME_SLOT_UNAVAILABLE": "Laiko laiko tarpas nebėra. Prašome pasirinkti kitą.", + "MEETING_SCHEDULER": "Susitikimų planuotojas", + "MIN": "mi" +} diff --git a/src/shared/resources/CometChatLocalize/resources/ms/translation.json b/src/shared/resources/CometChatLocalize/resources/ms/translation.json index a11509b..f8f94c9 100644 --- a/src/shared/resources/CometChatLocalize/resources/ms/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/ms/translation.json @@ -1,292 +1,302 @@ { - "USERS": "Pengguna", - "CHATS": "Sembang", - "GROUPS": "Kumpulan", - "MORE": "Lagi", - "MESSAGE_IMAGE": "📷 Imej", - "MESSAGE_FILE": "📁 Fail", - "MESSAGE_VIDEO": "📹 Video", - "MESSAGE_AUDIO": "🎵 Audio", - "CUSTOM_MESSAGE": "Anda mempunyai mesej", - "MISSED_VOICE_CALL": "Panggilan suara tidak dijawab", - "MISSED_VIDEO_CALL": "Panggilan video tidak dijawab", - "CUSTOM_MESSAGE_POLL": "📊 Undian", - "CUSTOM_MESSAGE_STICKER": "💟 Pelekat", - "CUSTOM_MESSAGE_DOCUMENT": "📃 Dokumen", - "SUGGEST_A_REPLY":"Cadangkan balasan", - "GENERATIONG_ICEBREAKER":"Menjana pemecah ais", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 Papan Putih", - "ONLINE": "Dalam Talian", - "NO_REPLIES_FOUND":"Tiada Balasan Ditemui", - "ADMINISTRATOR": "Pentadbir", - "MODERATOR": "Moderator", - "PARTICIPANT": "Peserta", - "PUBLIC": "Awam", - "GENERATING_REPLIES":"Menjana balasan", - "PRIVATE": "Persendirian", - "PASSWORD_PROTECTED": "Dilindungi", - "PRIVACY_AND_SECURITY": "Privasi dan Keselamatan", - "PREFERENCES": "Keutamaan", - "MEMBERS": "Ahli-ahli", - "TODAY": "Hari ini", - "YESTERDAY": "Semalam", - "SUNDAY": "Ahad", - "MONDAY": "Isnin", - "TUESDAY": "Selasa", - "WEDNESDAY": "Rabu", - "THURSDAY": "Khamis", - "FRIDAY": "Jumaat", - "SATURDAY": "Sabtu", - "TYPING": "menaip...", - "IS_TYPING": "sedang menaip...", - "CLOSE": "Tutup", - "ENTER_GROUP_NAME": "Masukkan nama kumpulan", - "ADD_MEMBERS": "Tambah Ahli", - "SEND_MESSAGE": "Hantar Mesej", - "UNBLOCK_USER": "Nyahsekat Pengguna", - "BLOCK_USER": "Sekat Pengguna", - "DELETE_AND_EXIT": "Padam dan Keluar", - "LEAVE_GROUP": "Tinggalkan Kumpulan", - "CREATE_GROUP": "Cipta Kumpulan", - "SHARED_MEDIA": "Media Kongsi", - "VIDEO_CALL": "Panggilan video", - "AUDIO_CALL": "Panggilan audio", - "LOADING": "Memuatkan...", - "REPLY": "jawapan", - "REPLIES": "balasannya", - "LAUNCH": "Pelancaran", - "SHARED_COLLABORATIVE_DOCUMENT": "telah berkongsi dokumen kolaboratif", - "SHARED_COLLABORATIVE_WHITEBOARD": "telah berkongsi papan putih kolaboratif", - "CREATED_WHITEBOARD": "Anda telah membuat papan putih kolaboratif baru", - "CREATED_DOCUMENT": "Anda telah membuat dokumen kolaboratif baru", - "PHOTOS": "Foto", - "VIDEOS": "Video", - "DOCUMENT": "Dokumen", - "YOU_DELETED_THIS_MESSAGE": "⚠️ Anda memadam mesej ini", - "THIS_MESSAGE_DELETED": "⚠️ Mesej ini telah dipadamkan", - "VIEW_ON_YOUTUBE": "Lihat di Youtube", - "SEARCH": "Cari", - "NO_USERS_FOUND": "Tiada pengguna ditemui", - "ERROR": "Ralat", - "NO_GROUPS_FOUND": "Tiada kumpulan ditemui", - "NO_CHATS_FOUND": "Tiada sembang ditemui", - "MEDIA_MESSAGE": "Mesej media", - "INCOMING_AUDIO_CALL": "Panggilan audio masuk", - "INCOMING_VIDEO_CALL": "Panggilan video masuk", - "DECLINE": "Tolak", - "ACCEPT": "Terima", - "CALL_INITIATED": "Panggilan dimulakan", - "OUTGOING_AUDIO_CALL": "Panggilan audio keluar", - "OUTGOING_VIDEO_CALL": "Panggilan video keluar", - "CALL_REJECTED": "Panggilan ditolak", - "REJECTED_CALL": "panggilan ditolak", - "CALL_ACCEPTED": "Panggilan diterima", - "JOINED": "menyertai", - "LEFT_THE_CALL": "meninggalkan panggilan", - "UNANSWERED_AUDIO_CALL": "Panggilan audio tidak dijawab", - "UNANSWERED_VIDEO_CALL": "Panggilan video tidak dijawab", - "CALL_ENDED": "Panggilan berakhir", - "CANCELLED_CALL": "Panggilan Dibatalkan", - "CALL_BUSY": "Panggilan sibuk", - "CALLING": "Memanggil...", - "ADD": "Tambah", - "NO_BANNED_MEMBERS_FOUND": "Tiada ahli yang diharamkan ditemui", - "BANNED_MEMBERS": "Ahli diharamkan", - "NAME": "Nama", - "SCOPE": "Skop", - "UNBAN": "Unban", - "SELECT_GROUP_TYPE": "Pilih jenis kumpulan", - "ENTER_GROUP_PASSWORD": "Masukkan kata laluan kumpulan", - "CREATE": "Cipta", - "CREATE_POLL": "Buat Undian", - "QUESTION": "Soalan", - "ENTER_YOUR_QUESTION": "Masukkan soalan anda", - "OPTIONS": "Pilihan", - "ENTER_YOUR_OPTION": "Masukkan pilihan anda", - "ADD_NEW_OPTION": "Tambah opsyen baru", - "VIEW_MEMBERS": "Lihat Ahli", - "DETAILS": "Perincian", - "NOTIFICATIONS": "Pemberitahuan", - "OTHER": "Lain-lain", - "HELP": "Bantuan", - "REPORT_PROBLEM": "Laporkan Masalah", - "GROUP_MEMBERS": "Ahli Kumpulan", - "BAN": "Ban", - "KICK": "Kick", - "PICK_YOUR_EMOJI": "Pilih emoji anda", - "PRIVATE_GROUP": "Kumpulan Persendirian", - "PROTECTED_GROUP": "Kumpulan Terlindung", - "VISIT": "Lawati", - "ATTACH": "Lampirkan", - "ATTACH_FILE": "Lampirkan fail", - "ATTACH_VIDEO": "Lampirkan video", - "ATTACH_AUDIO": "Lampirkan audio", - "ATTACH_IMAGE": "Lampirkan imej", - "COLLABORATE_USING_DOCUMENT": "Berkolaborasi menggunakan dokumen", - "COLLABORATE_USING_WHITEBOARD": "Berkolaborasi menggunakan papan putih", - "EMOJI": "Emotikon", - "ENTER_YOUR_MESSAGE_HERE": "Masukkan mesej anda di sini", - "NO_MESSAGES_FOUND": "Tiada mesej ditemui", - "THREAD": "Thread", - "COLLABORATIVE_DOCUMENT": "Dokumen Kerjasama", - "COLLABORATIVE_WHITEBOARD": "Papan Putih Kolaboratif", - "ADD_REACTION": "Tambah reaksi", - "NO_STICKERS_FOUND": "Tiada pelekat ditemui", - "REPLY_TO_THREAD": "Balas kepada bebenang", - "REPLY_IN_THREAD": "Balas dalam bebenang", - "DELETE_MESSAGE": "Padam mesej", - "EDIT_MESSAGE": "Sunting mesej", - "OWNER": "Pemilik", - "CHANGE_SCOPE": "Tukar Skop", - "STICKER": "Pelekat", - "LAST_ACTIVE_AT": "Terakhir Aktif di", - "VOICE_CALL": "Panggilan suara", - "VIEW_DETAIL": "Lihat Perincian", - "VOTES": "undi", - "VOTE": "undi", - "NO_VOTE": "Tiada undi", - "REACTED": "bertindak balas", - "ADDED": "campurkan", - "UNBANNED": "tidak diharamkan", - "MADE": "diperbuat", - "UNANSWERED_CALL": "Panggilan Tidak Dijawab", - "MISSED_AUDIO_CALL": "Panggilan audio tidak dijawab", - "ENTER_YOUR_PASSWORD": "Masukkan kata laluan anda", - "DOCS": "Dokumen", - "NO_RECORDS_FOUND": "Tiada rekod ditemui", - "LIVE_REACTION": "Reaksi Langsung", - "SMILEY_PEOPLE": "Smiley & People", - "ANIMALES_NATURE": "Haiwan & Alam Semula Jadi", - "FOOD_DRINK": "Makanan & Minuman", - "ACTIVITY": "Aktiviti", - "TRAVEL_PLACES": "Perjalanan & Tempat", - "OBJECTS": "Objek", - "SYMBOLS": "Simbol", - "FLAGS": "Bendera", - "SENT": "Dihantar", - "SEEN": "Seen", - "DELIVERED": "Dihantar", - "TRANSLATE_MESSAGE": "Terjemah mesej", - "LEFT": "ditinggalkan", - "KICKED": "ditendang", - "BANNED": "diharamkan", - "NEW_MESSAGES": "mesej baru", - "NEW_MESSAGE": "mesej baru", - "JUMP": "Lompat", - "SELECT_VIDEO_SOURCE": "Pilih Sumber video", - "SELECT_INPUT_AUDIO_SOURCE": "Pilih sumber audio input", - "SELECT_OUTPUT_AUDIO_SOURCE": "Pilih sumber audio output", - "INITIATED_GROUP_CALL": "telah memulakan panggilan kumpulan", - "YOU_INITIATED_GROUP_CALL": "Anda telah memulakan panggilan kumpulan", - "IGNORE": "Abaikan", - "ON_ANOTHER_CALL": "adalah pada panggilan lain", - "CREATING": "Mencipta", - "AVATAR": "Avatar", - "GROUP_NAME_BLANK": "Nama kumpulan tidak boleh kosong", - "GROUP_TYPE_BLANK": "Jenis kumpulan meriam tidak kosong", - "GROUP_PASSWORD_BLANK": "Kata laluan kumpulan tidak boleh kosong", - "POLL_QUESTION_BLANK": "Soalan meriam tidak kosong", - "POLL_OPTION_BLANK": "Opsyen meriam tidak kosong", - "ONGOING_CALL": "Panggilan berterusan", - "YOU_ALREADY_ONGOING_CALL": "Anda sudah berada dalam panggilan berterusan", - "RESIZE": "Saiz semula", - "SETTINGS": "Tetapan", - "ACTIONS": "Tindakan", - "VIEW_PROFILE": "Lihat Profil", - "SEND_MESSAGE_IN_PRIVATE": "Hantar mesej secara peribadi", - "DELETE": "Padam", - "DELETE_CONFIRM": "Apakah Anda yakin ingin menghapus?", - "CANCEL": "Batal", - "LEAVE_CONFIRM": "Apakah Anda yakin ingin meninggalkan grup?", - "TRANSFER_CONFIRM": "Anda adalah pemilik kumpulan, sila pindahkan pemilikan kepada ahli sebelum meninggalkan kumpulan", - "ADDING": "Menambah...", - "TRANSFER": "Pemindahan", - "TRANSFERRING": "Memindahkan", - "YES": "Ya", - "NO": "Tidak", - "SOMETHING_WRONG": "Sesuatu yang tidak kena, sila cuba lagi", - "INVALID_GROUP_NAME": "Sila masukkan nama yang sah untuk kumpulan dan cuba lagi", - "INVALID_GROUP_TYPE": "Sila masukkan jenis yang sah untuk kumpulan dan cuba lagi", - "INVALID_PASSWORD": "Sila masukkan kata laluan yang sah untuk kumpulan dan cuba lagi", - "WRONG_PASSWORD": "Sila masukkan kata laluan yang betul dan cuba lagi", - "INVALID_POLL_QUESTION": "Sila masukkan soalan yang diperlukan sebelum membuat pengundian", - "INVALID_POLL_OPTION": "Sila masukkan jawapan yang diperlukan sebelum mencipta pengundian", - "SAME_LANGUAGE_MESSAGE": "Bahasa yang dipilih untuk terjemahan adalah sama dengan bahasa mesej asal", - "LEAVE": "Tinggalkan", - "CALLS": "Panggilan", - "CUSTOM_MESSAGE_LOCATION": "📍 Lokasi", - "OFFLINE": "Luar Talian", - "YOU": "Anda", - "PRIVACY": "Privasi", - "BLOCKED_USERS": "Pengguna Dihalang", - "YOU'VE_BLOCKED": "Anda telah menyekat", - "NO_PHOTOS": "Tiada Foto", - "NO_VIDEOS": "Tiada Video", - "NO_DOCUMENTS": "Tiada Dokumen", - "JOIN": "Sertai", - "DELETE_CONFIRM_MESSAGE": "Adakah anda ingin memadamkan perbualan ini? Perbualan ini akan dipadamkan dari semua peranti anda.", - "CHAT_ERROR_MESSAGE": "Tidak dapat memuatkan sembang. Sila cuba lagi", - "TRY_AGAIN": "CUBA LAGI", - "CONFIRM": "Sahkan", - "UNSAFE_CONTENT": "KANDUNGAN TIDAK SELAMAT", - "UNSAFE_CONFIRMATION": "ADAKAH ANDA PASTI MAHU MELIHAT KANDUNGAN YANG TIDAK SELAMAT INI", - "AUDIO_FILE": "FAIL AUDIO", - "SHARED_FILE": "FAIL DIKONGSI", - "OPEN_DOCUMENT": "DOCUMEN TERBUKA", - "OPEN_WHITEBOARD": "BUKA WHITEBOAR", - "PEOPLE_VOTED": "ORANG MENGUNDI", - "ADD_TO_CHAT": "TAMBAH KE CHA", - "TAKE_PHOTO": "AMBIL FOTO", - "SET_THE_ANSWERS": "TETAPKAN JAWAPANNYA", - "ADD_ANOTHER_ANSWER": "TAMBAH JAWAPAN LAIN", - "ANSWER": "JAWAPAN", - "IN_A_THREAD": "Dalam benang ⤵", - "ERROR_GROUP_CREATE": "Tidak dapat cipta Kumpulan", - "TRY_AGAIN_LATER": "Sila cuba lagi kemudian", - "CONTINUE": "Teruskan", - "GROUP_NAME_MAX": "Maksimum 25 charector dibenarkan dalam Nama Kumpulan", - "PASSWORD_MAX": "Maksimum 16 charector dibenarkan dalam kata laluan kumpulan", - "GROUP_PASSWORD": "Kata laluan kumpulan", - "INCORRECT_PASSWORD": "Kata laluan yang salah", - "OPEN_WHITEBOARD_TO_DRAW": "Buka papan putih untuk menarik bersama", - "CALL_HISTORY": "Sejarah Panggilan", - "NO_CALL_HISTORY": "Tiada panggilan telah dibuat lagi", - "CALL_DETAILS": "Butiran Panggilan", - "CONFERENCE_CALL": "panggilan persidangan", - "NEW_GROUP": "Kumpulan Baru", - "TRANSFER_OWNERSHIP": "Pindahkan Pemilikan", - "COPY_MESSAGE": "Salin", - "SHARE": "Berkongsi", - "OPEN_DOCUMENT_TO_DRAW": "Buka dokumen untuk dilukis bersama", - "INFORMATION": "Maklumat", - "COPY_TEXT": "Salin Teks", - "FORWARD": "Ke hadapan", - "TEXT": "Teks", - "INCOMING_CALL": "Panggilan masuk", - "OUTGOING_CALL": "Panggilan keluar", - "MISSED_CALL": "Panggilan tidak dijawab", - "NEW_CONVERSATION": "Sembang Baru", - "FORWARDING": "Menghantar mesej..", - "READ": "Baca", - "No_RECIPIENT": "Tiada Penerima", - "RECEIPT_INFORMATION": "Maklumat Resit", - "MESSAGE": "Pesanan", - "GENERATING_SUMMARY":"Menjana Ringkasan", - "CONVERSATION_SUMMARY":"Ringkasan Perbualan", - "GENERATE_SUMMARY":"Menjana ringkasan", - "COMETCHAT_BOT_FIRST_MESSAGE":"Bagaimana saya boleh membantu anda dalam perbualan ini? Sila tanya saya soalan dan saya akan memberi nasihat kepada anda 🙂", - "COMETCHAT_ASK_BOT":"Tanya", - "COMETCHAT_ASK_AI_BOT":"Tanya AI Bots", - "COMETCHAT_ASK_BOT_SUBTITLE":"AI Bot", - "FORM_COMPLETION_MESSAGE": "Terima kasih kerana mengisi borang.", - "FORM":"Borang", - "CARD":"Kad", - "RECORDINGS":"Rakaman", - "PARTICIPANTS":"Peserta", - "NO_PARTICIPANTS":"Tiada Peserta", - "NO_RECORDINGS":"Tiada Rakaman", - "CALL_LOGS":"Log Panggilan", - "CANCELLED_AUDIO_CALL":"Panggilan audio yang dibatalkan", - "CANCELLED_VIDEO_CALL":"Panggilan video dibatalkan", - "MICROPHONE_PERMISSION":"Kami menggunakan mikrofon untuk merakam dan berkongsi mesej audio. Pergi ke tetapan untuk membolehkan akses mikrofon" -} \ No newline at end of file + "USERS": "Pengguna", + "CHATS": "Sembang", + "GROUPS": "Kumpulan", + "MORE": "Lagi", + "MESSAGE_IMAGE": "📷 Imej", + "MESSAGE_FILE": "📁 Fail", + "MESSAGE_VIDEO": "📹 Video", + "MESSAGE_AUDIO": "🎵 Audio", + "CUSTOM_MESSAGE": "Anda mempunyai mesej", + "MISSED_VOICE_CALL": "Panggilan suara terlepas", + "MISSED_VIDEO_CALL": "Panggilan video terlepas", + "CUSTOM_MESSAGE_POLL": "📊 Undian", + "CUSTOM_MESSAGE_STICKER": "💟 Pelekat", + "CUSTOM_MESSAGE_DOCUMENT": "📃 Dokumen", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 Papan putih", + "ONLINE": "Dalam Talian", + "ADMINISTRATOR": "Pentadbir", + "MODERATOR": "penyampai", + "PARTICIPANT": "Peserta", + "SUGGEST_A_REPLY": "Cadangkan balasan", + "GENERATIONG_ICEBREAKER": "Menjana pemecah ais", + "PUBLIC": "Awam", + "NO_REPLIES_FOUND": "Tiada Balasan Ditemui", + "PRIVATE": "Persendirian", + "PASSWORD_PROTECTED": "Kata Laluan Dilindungi", + "PRIVACY_AND_SECURITY": "Privasi dan Keselamatan", + "PREFERENCES": "Keutamaan", + "MEMBERS": "Ahli", + "TODAY": "Hari ini", + "YESTERDAY": "Semalam", + "SUNDAY": "Ahad", + "MONDAY": "Isnin", + "TUESDAY": "Selasa", + "WEDNESDAY": "Rabu", + "THURSDAY": "Khamis", + "FRIDAY": "Jumaat", + "SATURDAY": "Sabtu", + "TYPING": "menaip...", + "IS_TYPING": "sedang menaip...", + "CLOSE": "Tutup", + "ENTER_GROUP_NAME": "Masukkan nama kumpulan", + "ADD_MEMBERS": "Tambah Ahli", + "SEND_MESSAGE": "Hantar Mesej", + "UNBLOCK_USER": "Nyahsekat Pengguna", + "BLOCK_USER": "Sekat Pengguna", + "DELETE_AND_EXIT": "Padam dan Keluar", + "LEAVE_GROUP": "Tinggalkan Kumpulan", + "CREATE_GROUP": "Buat Kumpulan", + "SHARED_MEDIA": "Media Dikongsi", + "VIDEO_CALL": "Panggilan video", + "AUDIO_CALL": "Panggilan audio", + "LOADING": "Memuatkan...", + "REPLY": "jawapan", + "REPLIES": "balasannya", + "LAUNCH": "Pelancaran", + "SHARED_COLLABORATIVE_DOCUMENT": "telah berkongsi dokumen kolaboratif", + "SHARED_COLLABORATIVE_WHITEBOARD": "telah berkongsi papan putih kolaboratif", + "CREATED_WHITEBOARD": "Anda telah mencipta papan putih kolaboratif baru", + "CREATED_DOCUMENT": "Anda telah mencipta dokumen SomCollaborative baru", + "PHOTOS": "Gambar", + "VIDEOS": "Video", + "DOCUMENT": "Dokumen", + "MESSAGE_IS_DELETED": "Mesej dipadamkan", + "THIS_MESSAGE_DELETED": "⚠️ Mesej ini telah dipadamkan", + "VIEW_ON_YOUTUBE": "Lihat di Youtube", + "SEARCH": "Cari", + "NO_USERS_FOUND": "Tiada pengguna ditemui", + "ERROR": "Ralat", + "NO_GROUPS_FOUND": "Tiada kumpulan ditemui", + "NO_CHATS_FOUND": "Tiada sembang ditemui", + "MEDIA_MESSAGE": "Mesej media", + "INCOMING_AUDIO_CALL": "Panggilan audio masuk", + "INCOMING_VIDEO_CALL": "Panggilan video masuk", + "DECLINE": "Menolak", + "ACCEPT": "Terima", + "CALL_INITIATED": "Panggilan dimulakan", + "OUTGOING_AUDIO_CALL": "Panggilan audio keluar", + "OUTGOING_VIDEO_CALL": "Panggilan video keluar", + "CALL_REJECTED": "Panggilan ditolak", + "REJECTED_CALL": "Panggilan ditolak", + "CALL_ACCEPTED": "Panggilan diterima", + "JOINED": "menyertai", + "LEFT_THE_CALL": "meninggalkan panggilan", + "UNANSWERED_AUDIO_CALL": "Panggilan audio yang tidak dijawab", + "UNANSWERED_VIDEO_CALL": "Panggilan video yang tidak dijawab", + "CALL_ENDED": "Panggilan berakhir", + "CANCELLED_CALL": "Panggilan dibatalkan", + "CALL_BUSY": "Panggilan sibuk", + "CALLING": "Memanggil...", + "ADD": "Tambah", + "NO_BANNED_MEMBERS_FOUND": "Tiada ahli yang dilarang ditemui", + "BANNED_MEMBERS": "Ahli yang diharamkan", + "NAME": "Nama", + "SCOPE": "Skop", + "UNBAN": "Unban", + "SELECT_GROUP_TYPE": "Pilih jenis kumpulan", + "ENTER_GROUP_PASSWORD": "Masukkan kata laluan kumpulan", + "CREATE": "Buat", + "CREATE_POLL": "Buat Undian", + "QUESTION": "Soalan", + "ENTER_YOUR_QUESTION": "Masukkan soalan anda", + "OPTIONS": "Pilihan", + "ENTER_YOUR_OPTION": "Masukkan pilihan anda", + "ADD_NEW_OPTION": "Tambah pilihan baru", + "VIEW_MEMBERS": "Lihat Ahli", + "DETAILS": "Butiran", + "NOTIFICATIONS": "Pemberitahuan", + "OTHER": "Lain-lain", + "HELP": "Bantuan", + "REPORT_PROBLEM": "Laporkan Masalah", + "GROUP_MEMBERS": "Ahli Kumpulan", + "BAN": "Larangan", + "KICK": "Tendang", + "PICK_YOUR_EMOJI": "Pilih emoji anda", + "PRIVATE_GROUP": "Kumpulan Swasta", + "PROTECTED_GROUP": "Kumpulan yang dilindungi", + "VISIT": "Lawatan", + "ATTACH": "Lampirkan", + "ATTACH_FILE": "Lampirkan fail", + "ATTACH_VIDEO": "Lampirkan video", + "ATTACH_AUDIO": "Lampirkan audio", + "ATTACH_IMAGE": "Lampirkan imej", + "COLLABORATIVE_DOCUMENT": "Dokumen Kolaboratif", + "COLLABORATIVE_WHITEBOARD": "Papan Putih Kolaboratif", + "COLLABORATE_USING_DOCUMENT": "Bekerjasama menggunakan dokumen", + "COLLABORATE_USING_WHITEBOARD": "Bekerjasama menggunakan papan putih", + "EMOJI": "Emotikon", + "ENTER_YOUR_MESSAGE_HERE": "Masukkan mesej anda di sini", + "NO_MESSAGES_FOUND": "Tiada mesej di sini lagi...", + "THREAD": "Benang", + "ADD_REACTION": "Tambah tindak balas", + "NO_STICKERS_FOUND": "Tiada pelekat ditemui", + "REPLY_TO_THREAD": "Balas kepada thread", + "REPLY_IN_THREAD": "Balas dalam thread", + "DELETE_MESSAGE": "Padamkan mesej", + "EDIT_MESSAGE": "Edit Mesej", + "OWNER": "Pemilik", + "CHANGE_SCOPE": "Tukar Skop", + "STICKER": "Pelekat", + "LAST_ACTIVE_AT": "Terakhir Aktif Di", + "VOICE_CALL": "Panggilan suara", + "VIEW_DETAIL": "Lihat Butiran", + "VOTES": "undi", + "VOTE": "undi", + "NO_VOTE": "Tiada undi", + "REACTED": "bertindak balas", + "ADDED": "campurkan", + "UNBANNED": "tidak diharamkan", + "MADE": "diperbuat", + "UNANSWERED_CALL": "Panggilan tidak dijawab", + "MISSED_AUDIO_CALL": "Panggilan audio terlepas", + "ENTER_YOUR_PASSWORD": "Masukkan kata laluan anda", + "DOCS": "Dokumen", + "NO_RECORDS_FOUND": "Tiada rekod ditemui", + "LIVE_REACTION": "Reaksi Langsung", + "SMILEY_PEOPLE": "Senyuman & Orang", + "ANIMALES_NATURE": "Haiwan & Alam", + "FOOD_DRINK": "Makanan & Minuman", + "ACTIVITY": "Aktiviti", + "TRAVEL_PLACES": "Perjalanan & Tempat", + "OBJECTS": "Objek", + "SYMBOLS": "Simbol", + "FLAGS": "Bendera", + "SENT": "Menghantar", + "SEEN": "Dilihat", + "DELIVERED": "Dihantar", + "TRANSLATE_MESSAGE": "Terjemah mesej", + "LEFT": "ditinggalkan", + "KICKED": "ditendang", + "BANNED": "diharamkan", + "NEW_MESSAGES": "mesej baru", + "NEW_MESSAGE": "mesej baru", + "JUMP": "Lompat", + "SELECT_VIDEO_SOURCE": "Pilih sumber video", + "SELECT_INPUT_AUDIO_SOURCE": "Pilih sumber audio input", + "SELECT_OUTPUT_AUDIO_SOURCE": "Pilih sumber audio output", + "INITIATED_GROUP_CALL": "telah memulakan panggilan kumpulan", + "YOU_INITIATED_GROUP_CALL": "Anda telah memulakan panggilan kumpulan", + "IGNORE": "Abaikan", + "ON_ANOTHER_CALL": "sedang dalam panggilan lain", + "CREATING": "Mewujudkan", + "AVATAR": "Avatar", + "ONGOING_CALL": "Panggilan berterusan", + "YOU_ALREADY_ONGOING_CALL": "Anda sudah dalam panggilan berterusan", + "RESIZE": "Ubah saiz", + "SETTINGS": "Tetapan", + "ACTIONS": "Tindakan", + "VIEW_PROFILE": "Lihat Profil", + "SEND_MESSAGE_IN_PRIVATE": "Hantar mesej secara peribadi", + "DELETE": "Padam", + "DELETE_CONFIRM": "Adakah anda pasti mahu memadam?", + "CANCEL": "Batalkan", + "LEAVE_CONFIRM": "Adakah anda pasti mahu meninggalkan kumpulan itu?", + "TRANSFER_CONFIRM": "Anda adalah pemilik kumpulan; sila pindahkan pemilikan kepada ahli sebelum meninggalkan kumpulan", + "ADDING": "Menambah...", + "TRANSFER": "Pemindahan", + "TRANSFERRING": "Memindahkan", + "YES": "Ya", + "NO": "Tidak", + "SOMETHING_WRONG": "Sesuatu yang tidak kena; Sila cuba lagi.", + "INVALID_GROUP_NAME": "Sila masukkan nama yang sah untuk kumpulan dan cuba lagi", + "INVALID_PASSWORD": "Sila masukkan kata laluan yang sah untuk kumpulan dan cuba lagi", + "INVALID_GROUP_TYPE": "Sila masukkan jenis yang sah untuk kumpulan dan cuba lagi", + "WRONG_PASSWORD": "Sila masukkan kata laluan yang betul dan cuba lagi", + "INVALID_POLL_QUESTION": "Sila masukkan soalan yang diperlukan sebelum membuat pengundian", + "INVALID_POLL_OPTION": "Sila masukkan jawapan yang diperlukan sebelum membuat pengundian", + "SAME_LANGUAGE_MESSAGE": "Bahasa terpilih untuk terjemahan adalah serupa dengan bahasa mesej asal", + "LEAVE": "Tinggalkan", + "CUSTOM_MESSAGE_LOCATION": "📍 Lokasi", + "IN_A_THREAD": "Dalam benang ⤵", + "CALLS": "Panggilan", + "OFFLINE": "Luar talian", + "YOU": "Anda", + "PRIVACY": "Privasi", + "BLOCKED_USERS": "Pengguna yang disekat", + "YOU'VE_BLOCKED": "Anda telah menyekat", + "NO_PHOTOS": "Tiada Foto", + "NO_VIDEOS": "Tiada Video", + "NO_DOCUMENTS": "Tiada Dokumen", + "GENERATING_REPLIES": "Menjana balasan", + "JOIN": "Sertai", + "DELETE_CONFIRM_MESSAGE": "Adakah anda ingin memadamkan perbualan ini? Perbualan ini akan dipadamkan dari semua peranti anda.", + "CHAT_ERROR_MESSAGE": "Tidak boleh memuatkan sembang. Sila cuba lagi", + "TRY_AGAIN": "CUBA LAGI", + "CONFIRM": "Sahkan", + "UNSAFE_CONTENT": "Kandungan Tidak Selamat", + "UNSAFE_CONFIRMATION": "Adakah anda pasti mahu melihat kandungan yang tidak selamat ini", + "AUDIO_FILE": "Fail Audio", + "SHARED_FILE": "Fail Dikongsi", + "OPEN_DOCUMENT": "DOKUMEN TERBUKA", + "OPEN_WHITEBOARD": "BUKA PAPAN PUTIH", + "PEOPLE_VOTED": "orang mengundi", + "ADD_TO_CHAT": "Tambah ke Sembang", + "TAKE_PHOTO": "Ambil Gambar", + "SET_THE_ANSWERS": "TETAPKAN JAWAPAN", + "ADD_ANOTHER_ANSWER": "Tambah Jawapan Lain", + "ANSWER": "Jawapan", + "ERROR_GROUP_CREATE": "Tidak boleh membuat Kumpulan", + "TRY_AGAIN_LATER": "Sila cuba lagi nanti", + "CONTINUE": "Teruskan", + "GROUP_NAME_MAX": "Maksimum 25 kerektor dibenarkan dalam Nama Kumpulan", + "GROUP_PASSWORD_BLANK": "Sila masukkan kata laluan", + "PASSWORD_MAX": "Maksimum 16 charector dibenarkan dalam kata laluan kumpulan", + "GROUP_PASSWORD": "Kata laluan kumpulan", + "INCORRECT_PASSWORD": "Kata laluan yang salah", + "OPEN_WHITEBOARD_TO_DRAW": "Buka papan putih untuk menggambar bersama", + "CALL_HISTORY": "Sejarah Panggilan", + "NO_CALL_HISTORY": "Tiada panggilan telah dibuat lagi", + "CALL_DETAILS": "Butiran Panggilan", + "CONFERENCE_CALL": "panggilan persidangan", + "NEW_GROUP": "Kumpulan Baru", + "TRANSFER_OWNERSHIP": "Pemindahan Pemilikan", + "COPY_MESSAGE": "Salin", + "SHARE": "Kongsi", + "OPEN_DOCUMENT_TO_DRAW": "Buka dokumen untuk menggambar bersama", + "INFORMATION": "Maklumat mesej", + "FORWARD": "Ke hadapan", + "COPY_TEXT": "Salin Teks", + "TEXT": "Teks", + "INCOMING_CALL": "Panggilan masuk", + "OUTGOING_CALL": "Panggilan keluar", + "MISSED_CALL": "Panggilan tidak dijawab", + "GROUP_NAME_BLANK": "Nama kumpulan tidak boleh kosong", + "GROUP_TYPE_BLANK": "Jenis kumpulan Tidak boleh kosong", + "POLL_QUESTION_BLANK": "Soalan canaut kosong", + "POLL_OPTION_BLANK": "Pilihan Tidak boleh kosong", + "NEW_CONVERSATION": "Sembang Baru", + "FORWARDING": "Menghantar mesej...", + "READ": "Baca", + "No_RECIPIENT": "Tiada Penerima", + "RECEIPT_INFORMATION": "Maklumat Resit", + "MESSAGE": "Pesanan", + "GENERATING_SUMMARY": "Menjana Ringkasan", + "CONVERSATION_SUMMARY": "Ringkasan Perbualan", + "GENERATE_SUMMARY": "Menjana ringkasan", + "COMETCHAT_BOT_FIRST_MESSAGE": "Bagaimana saya boleh membantu anda dalam perbualan ini? Sila tanya saya soalan dan saya akan memberi nasihat kepada anda 🙂", + "COMETCHAT_ASK_BOT": "Tanya", + "COMETCHAT_ASK_AI_BOT": "Tanya AI Bots", + "COMETCHAT_ASK_BOT_SUBTITLE": "AI Bot", + "FORM_COMPLETION_MESSAGE": "Terima kasih kerana mengisi borang.", + "FORM": "Borang", + "CARD": "Kad", + "RECORDINGS": "Rakaman", + "PARTICIPANTS": "Peserta", + "NO_PARTICIPANTS": "Tiada Peserta", + "NO_RECORDINGS": "Tiada Rakaman", + "CALL_LOGS": "Log Panggilan", + "CANCELLED_AUDIO_CALL": "Panggilan audio yang dibatalkan", + "CANCELLED_VIDEO_CALL": "Panggilan video dibatalkan", + "MEET_WITH": "Bertemu dengan", + "MIN_MEETING": "mesyuarat min", + "MORE_TIMES": "Lebih banyak kali", + "SELECT_DAY": "Pilih Hari", + "SELECT_TIME": "Pilih Masa", + "NO_TIME_SLOT_AVAILABLE": "Tiada slot masa tersedia pada tarikh ini. Sila cuba tarikh lain.", + "BOOK_NEW_SLOT": "Tempah slot baru", + "TRY_AGAIN_CAMEL": "Cuba lagi", + "TIME_SLOT_UNAVAILABLE": "Slot masa tidak lagi tersedia. Sila pilih yang lain.", + "MEETING_SCHEDULER": "Penjadual Mesyuarat", + "MIN": "mi" +} diff --git a/src/shared/resources/CometChatLocalize/resources/pt/translation.json b/src/shared/resources/CometChatLocalize/resources/pt/translation.json index 358eae7..11e2da4 100644 --- a/src/shared/resources/CometChatLocalize/resources/pt/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/pt/translation.json @@ -1,292 +1,302 @@ { - "USERS": "Usuários", - "CHATS": "Bate-papo", - "GROUPS": "Grupos", - "MORE": "Mais", - "MESSAGE_IMAGE": "📷 Imagem", - "MESSAGE_FILE": "📁 Arquivo", - "MESSAGE_VIDEO": "📹 Vídeo", - "MESSAGE_AUDIO": "🎵 Áudio", - "CUSTOM_MESSAGE": "Você tem uma mensagem", - "MISSED_VOICE_CALL": "Chamada de voz perdida", - "MISSED_VIDEO_CALL": "Chamada de vídeo perdida", - "CUSTOM_MESSAGE_POLL": "📊 Enquete", - "CUSTOM_MESSAGE_STICKER": "💟 Adesivo", - "CUSTOM_MESSAGE_DOCUMENT": "📃 Documento", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 quadro branco", - "ONLINE": "On-line", - "ADMINISTRATOR": "Administrador", - "NO_REPLIES_FOUND":"Nenhuma resposta encontrada", - "MODERATOR": "Moderador", - "PARTICIPANT": "Participante", - "GENERATING_REPLIES":"Gerando respostas", - "PUBLIC": "Público", - "SUGGEST_A_REPLY":"Sugira uma resposta", - "GENERATIONG_ICEBREAKER":"Gerando quebra-gelos", - "PRIVATE": "Privado", - "PASSWORD_PROTECTED": "Protegido por senha", - "PRIVACY_AND_SECURITY": "Privacidade e Segurança", - "PREFERENCES": "Preferências", - "MEMBERS": "Membros", - "TODAY": "Hoje", - "YESTERDAY": "Ontem", - "SUNDAY": "domingo", - "MONDAY": "Segunda-feira", - "TUESDAY": "terça", - "WEDNESDAY": "Quarta-feira", - "THURSDAY": "Quinta-feira", - "FRIDAY": "Sexta-feira", - "SATURDAY": "sábado", - "TYPING": "digitando...", - "IS_TYPING": "está digitando...", - "CLOSE": "Fechar", - "ENTER_GROUP_NAME": "Digite o nome do grupo", - "ADD_MEMBERS": "Adicionar Membros", - "SEND_MESSAGE": "Enviar Mensagem", - "UNBLOCK_USER": "Desbloquear Usuário", - "BLOCK_USER": "Bloquear usuário", - "DELETE_AND_EXIT": "Excluir e sair", - "LEAVE_GROUP": "Sair do grupo", - "CREATE_GROUP": "Criar grupo", - "SHARED_MEDIA": "Mídia compartilhada", - "VIDEO_CALL": "Chamada de vídeo", - "AUDIO_CALL": "Chamada de áudio", - "LOADING": "Carregando...", - "REPLY": "resposta", - "REPLIES": "respostas", - "LAUNCH": "Lançamento", - "SHARED_COLLABORATIVE_DOCUMENT": "compartilhou um documento colaborativo", - "SHARED_COLLABORATIVE_WHITEBOARD": "compartilhou um quadro de comunicações colaborativo", - "CREATED_WHITEBOARD": "Você criou um novo quadro de comunicações colaborativo", - "CREATED_DOCUMENT": "Você criou um novo documento colaborativo", - "PHOTOS": "Fotos", - "VIDEOS": "Vídeos", - "DOCUMENT": "Documento", - "YOU_DELETED_THIS_MESSAGE": "⚠️ Você excluiu esta mensagem", - "THIS_MESSAGE_DELETED": "⚠️ Esta mensagem foi excluída", - "VIEW_ON_YOUTUBE": "Ver no Youtube", - "SEARCH": "Pesquisar", - "NO_USERS_FOUND": "Nenhum usuário encontrado", - "ERROR": "Erro", - "NO_GROUPS_FOUND": "Nenhum grupo encontrado", - "NO_CHATS_FOUND": "Não foram encontrados chats", - "MEDIA_MESSAGE": "Mensagem de mídia", - "INCOMING_AUDIO_CALL": "Chamada de áudio recebida", - "INCOMING_VIDEO_CALL": "Chamada de vídeo recebida", - "DECLINE": "Declínio", - "ACCEPT": "Aceitar", - "CALL_INITIATED": "Chamada iniciada", - "OUTGOING_AUDIO_CALL": "Chamada de áudio de saída", - "OUTGOING_VIDEO_CALL": "Chamada de vídeo de saída", - "CALL_REJECTED": "Chamada rejeitada", - "REJECTED_CALL": "chamada rejeitada", - "CALL_ACCEPTED": "Chamada aceita", - "JOINED": "ingressou", - "LEFT_THE_CALL": "deixou a chamada", - "UNANSWERED_AUDIO_CALL": "Chamada de áudio sem resposta", - "UNANSWERED_VIDEO_CALL": "Chamada de vídeo sem resposta", - "CALL_ENDED": "Chamada encerrada", - "CANCELLED_CALL": "Chamada cancelada", - "CALL_BUSY": "Ligue ocupado", - "CALLING": "Chamando...", - "ADD": "Adicionar", - "NO_BANNED_MEMBERS_FOUND": "Não foram encontrados membros proibidos", - "BANNED_MEMBERS": "Membros Banidos", - "NAME": "Nome", - "SCOPE": "Âmbito", - "UNBAN": "Unban", - "SELECT_GROUP_TYPE": "Selecionar tipo de grupo", - "ENTER_GROUP_PASSWORD": "Digite a senha do grupo", - "CREATE": "Criar", - "CREATE_POLL": "Criar enquete", - "QUESTION": "Pergunta", - "ENTER_YOUR_QUESTION": "Insira sua pergunta", - "OPTIONS": "Opções", - "ENTER_YOUR_OPTION": "Introduza a sua opção", - "ADD_NEW_OPTION": "Adicionar nova opção", - "VIEW_MEMBERS": "Ver Membros", - "DETAILS": "Detalhes", - "NOTIFICATIONS": "Notificações", - "OTHER": "Outros", - "HELP": "Ajuda", - "REPORT_PROBLEM": "Denunciar um problema", - "GROUP_MEMBERS": "Membros do Grupo", - "BAN": "Proibição", - "KICK": "Chute", - "PICK_YOUR_EMOJI": "Escolha o seu emoji", - "PRIVATE_GROUP": "Grupo Privado", - "PROTECTED_GROUP": "Grupo protegido", - "VISIT": "Visitar", - "ATTACH": "Anexar", - "ATTACH_FILE": "Anexar arquivo", - "ATTACH_VIDEO": "Anexar vídeo", - "ATTACH_AUDIO": "Anexar áudio", - "ATTACH_IMAGE": "Anexar imagem", - "COLLABORATE_USING_DOCUMENT": "Colaborar usando um documento", - "COLLABORATE_USING_WHITEBOARD": "Colaborar usando um quadro branco", - "EMOJI": "Emoji", - "ENTER_YOUR_MESSAGE_HERE": "Introduza aqui a sua mensagem", - "NO_MESSAGES_FOUND": "Nenhuma mensagem encontrada", - "THREAD": "Rosca", - "COLLABORATIVE_DOCUMENT": "Documento Colaborativo", - "COLLABORATIVE_WHITEBOARD": "Quadro Colaborativo", - "ADD_REACTION": "Adicionar reação", - "NO_STICKERS_FOUND": "Não foram encontrados adesivos", - "REPLY_TO_THREAD": "Responder ao thread", - "REPLY_IN_THREAD": "Responder no tópico", - "DELETE_MESSAGE": "Excluir mensagem", - "EDIT_MESSAGE": "Editar mensagem", - "OWNER": "Proprietário", - "CHANGE_SCOPE": "Alterar escopo", - "STICKER": "Adesivo", - "LAST_ACTIVE_AT": "Último ativo em", - "VOICE_CALL": "Chamada de voz", - "VIEW_DETAIL": "Ver detalhes", - "VOTES": "vota", - "VOTE": "votar", - "NO_VOTE": "Sem votação", - "REACTED": "reagiu", - "ADDED": "adicionada", - "UNBANNED": "não banido", - "MADE": "fez", - "UNANSWERED_CALL": "Chamada não atendida", - "MISSED_AUDIO_CALL": "Chamada de áudio perdida", - "ENTER_YOUR_PASSWORD": "Digite sua senha", - "DOCS": "Documentos", - "NO_RECORDS_FOUND": "Nenhum registro encontrado", - "LIVE_REACTION": "Reação ao vivo", - "SMILEY_PEOPLE": "Smileys & Pessoas", - "ANIMALES_NATURE": "Animais & Natureza", - "FOOD_DRINK": "Comidas & Bebidas", - "ACTIVITY": "Atividade", - "TRAVEL_PLACES": "Viagens e lugares", - "OBJECTS": "Objetos", - "SYMBOLS": "Símbolos", - "FLAGS": "Bandeiras", - "SENT": "Enviado", - "SEEN": "Visto", - "DELIVERED": "Entregue", - "TRANSLATE_MESSAGE": "Traduzir mensagem", - "LEFT": "esquerdo", - "KICKED": "chutado", - "BANNED": "banido", - "NEW_MESSAGES": "novas mensagens", - "NEW_MESSAGE": "nova mensagem", - "JUMP": "Saltar", - "SELECT_VIDEO_SOURCE": "Selecionar fonte de vídeo", - "SELECT_INPUT_AUDIO_SOURCE": "Selecionar fonte de áudio de entrada", - "SELECT_OUTPUT_AUDIO_SOURCE": "Selecionar fonte de áudio de saída", - "INITIATED_GROUP_CALL": "iniciou uma chamada de grupo", - "YOU_INITIATED_GROUP_CALL": "Você iniciou uma chamada em grupo", - "IGNORE": "Ignorar", - "ON_ANOTHER_CALL": "está em outra chamada", - "CREATING": "Criando", - "AVATAR": "Avatar", - "GROUP_NAME_BLANK": "O nome do grupo não pode estar em branco", - "GROUP_TYPE_BLANK": "Tipo de grupo não pode estar em branco", - "GROUP_PASSWORD_BLANK": "A senha do grupo não pode estar em branco", - "POLL_QUESTION_BLANK": "Pergunta não pode ser em branco", - "POLL_OPTION_BLANK": "Opção não estar em branco", - "ONGOING_CALL": "Chamada em curso", - "YOU_ALREADY_ONGOING_CALL": "Você já está em uma chamada em andamento", - "RESIZE": "Redimensionar", - "SETTINGS": "Configurações", - "ACTIONS": "Ações", - "VIEW_PROFILE": "Ver Perfil", - "SEND_MESSAGE_IN_PRIVATE": "Enviar mensagem privada", - "DELETE": "Excluir", - "DELETE_CONFIRM": "Tem certeza de que deseja excluir?", - "CANCEL": "Cancelar", - "LEAVE_CONFIRM": "Tem certeza que quer deixar o grupo?", - "TRANSFER_CONFIRM": "Você é o proprietário do grupo. Por favor, transfira a propriedade para um membro antes de sair do grupo", - "ADDING": "Adicionando...", - "TRANSFER": "Transferir", - "TRANSFERRING": "Transferindo", - "YES": "sim", - "NO": "Não", - "SOMETHING_WRONG": "Algo deu errado, tente novamente", - "INVALID_GROUP_NAME": "Insira um nome válido para o grupo e tente novamente", - "INVALID_GROUP_TYPE": "Insira um tipo válido para o grupo e tente novamente", - "INVALID_PASSWORD": "Insira uma senha válida para o grupo e tente novamente", - "WRONG_PASSWORD": "Digite a senha correta e tente novamente", - "INVALID_POLL_QUESTION": "Insira a pergunta necessária antes de criar uma enquete", - "INVALID_POLL_OPTION": "Insira a resposta necessária antes de criar uma enquete", - "SAME_LANGUAGE_MESSAGE": "O idioma selecionado para tradução é semelhante ao idioma da mensagem original", - "LEAVE": "Sair", - "CALLS": "Chamadas", - "CUSTOM_MESSAGE_LOCATION": "📍 Localização", - "OFFLINE": "Offline", - "YOU": "Você", - "PRIVACY": "Privacidade", - "BLOCKED_USERS": "Usuários bloqueados", - "YOU'VE_BLOCKED": "Você bloqueou", - "NO_PHOTOS": "Sem Fotos", - "NO_VIDEOS": "Sem vídeos", - "NO_DOCUMENTS": "Sem Documentos", - "JOIN": "Junte-se", - "DELETE_CONFIRM_MESSAGE": "Você gostaria de excluir essa conversa? Essa conversa será excluída de todos os seus dispositivos.", - "CHAT_ERROR_MESSAGE": "Não consigo carregar bate-papos. Por favor, tente novamente", - "TRY_AGAIN": "TENTE NOVAMENTE", - "CONFIRM": "Confirmar", - "UNSAFE_CONTENT": "CONTEÚDO INSEGURO", - "UNSAFE_CONFIRMATION": "VOCÊ CERTAMENTE DESEJA VER ESSE CONTEÚDO INSEGURO?", - "AUDIO_FILE": "ARQUIVO DE ÁUDIO", - "SHARED_FILE": "ARQUIVO COMPARTILHADO", - "OPEN_DOCUMENT": "DOCUMENTO ABERTO", - "OPEN_WHITEBOARD": "QUADRO BRANCO ABERTO", - "PEOPLE_VOTED": "PESSOAS VOTAM", - "ADD_TO_CHAT": "ADICIONAR AO CHAT", - "TAKE_PHOTO": "TIRAR UMA FOTO", - "SET_THE_ANSWERS": "DEFINA AS RESPOSTAS", - "ADD_ANOTHER_ANSWER": "ADICIONAR OUTRA RESPOSTA", - "ANSWER": "RESPOSTA", - "IN_A_THREAD": "Em um tópico ⤵", - "ERROR_GROUP_CREATE": "Não é possível criar um grupo", - "TRY_AGAIN_LATER": "Por favor, tente novamente mais tarde", - "CONTINUE": "Continuar", - "GROUP_NAME_MAX": "Máximo de 25 caracteres permitidos no nome do grupo", - "PASSWORD_MAX": "Máximo de 16 caracteres permitidos na senha do grupo", - "GROUP_PASSWORD": "Senha do grupo", - "INCORRECT_PASSWORD": "Senha incorreta", - "OPEN_WHITEBOARD_TO_DRAW": "Abra o quadro branco para desenhar juntos", - "CALL_HISTORY": "Histórico de chamadas", - "NO_CALL_HISTORY": "Nenhuma chamada foi feita ainda", - "CALL_DETAILS": "Detalhes da chamada", - "CONFERENCE_CALL": "teleconferência", - "NEW_GROUP": "Novo grupo", - "TRANSFER_OWNERSHIP": "Transferir propriedade", - "COPY_MESSAGE": "Copiar", - "SHARE": "Compartilhar", - "OPEN_DOCUMENT_TO_DRAW": "Abra o documento para desenhar juntos", - "INFORMATION": "Informações", - "COPY_TEXT": "Copiar texto", - "FORWARD": "Avançar", - "TEXT": "Texto", - "INCOMING_CALL": "Chamada recebida", - "OUTGOING_CALL": "Chamada de saída", - "MISSED_CALL": "Chamada perdida", - "NEW_CONVERSATION": "Novo bate-papo", - "FORWARDING": "Enviando mensagens..", - "READ": "Leia", - "No_RECIPIENT": "Nenhum destinatário", - "RECEIPT_INFORMATION": "Informações sobre o recibo", - "MESSAGE": "Mensagem", - "GENERATING_SUMMARY":"Gerando resumo", - "CONVERSATION_SUMMARY":"Resumo da conversa", - "GENERATE_SUMMARY":"Gere um resumo", - "COMETCHAT_BOT_FIRST_MESSAGE":"Como posso te ajudar nessa conversa? Por favor, me faça uma pergunta e eu vou te aconselhar 🙂", - "COMETCHAT_ASK_BOT":"Pergunte", - "COMETCHAT_ASK_AI_BOT":"Pergunte aos robôs de IA", - "COMETCHAT_ASK_BOT_SUBTITLE":"Robô de IA", - "FORM_COMPLETION_MESSAGE": "Obrigado por preencher o formulário.", - "FORM":"Formulário", - "CARD":"Cartão", - "RECORDINGS":"Gravações", - "PARTICIPANTS":"Participantes", - "NO_PARTICIPANTS":"Sem participantes", - "NO_RECORDINGS":"Sem gravações", - "CALL_LOGS":"Registros de chamadas", - "CANCELLED_AUDIO_CALL":"Chamada de áudio cancelada", - "CANCELLED_VIDEO_CALL":"Chamada de vídeo cancelada", - "MICROPHONE_PERMISSION":"Usamos o microfone para gravar e compartilhar mensagens de áudio. Vá para as configurações para ativar o acesso ao microfone" -} \ No newline at end of file + "USERS": "Usuários", + "CHATS": "Conversa", + "GROUPS": "Grupos", + "MORE": "Mais", + "MESSAGE_IMAGE": "📷 Imagem", + "MESSAGE_FILE": "📁 Arquivo", + "MESSAGE_VIDEO": "📹 Vídeo", + "MESSAGE_AUDIO": "🎵 Áudio", + "CUSTOM_MESSAGE": "Você tem uma mensagem", + "MISSED_VOICE_CALL": "Chamada de voz perdida", + "MISSED_VIDEO_CALL": "Chamada de vídeo perdida", + "CUSTOM_MESSAGE_POLL": "📊 Enquete", + "CUSTOM_MESSAGE_STICKER": "💟 Adesivo", + "CUSTOM_MESSAGE_DOCUMENT": "📃 Documento", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 Quadro branco", + "ONLINE": "On-line", + "ADMINISTRATOR": "Administrador", + "MODERATOR": "apresentadora", + "PARTICIPANT": "Participante", + "SUGGEST_A_REPLY": "Sugira uma resposta", + "GENERATIONG_ICEBREAKER": "Gerando quebra-gelos", + "PUBLIC": "Público", + "NO_REPLIES_FOUND": "Nenhuma resposta encontrada", + "PRIVATE": "Privado", + "PASSWORD_PROTECTED": "Protegido por senha", + "PRIVACY_AND_SECURITY": "Privacidade e segurança", + "PREFERENCES": "Preferências", + "MEMBERS": "Membros", + "TODAY": "Hoje", + "YESTERDAY": "Ontem", + "SUNDAY": "domingo", + "MONDAY": "Segunda-feira", + "TUESDAY": "terça", + "WEDNESDAY": "Quarta-feira", + "THURSDAY": "Quinta-feira", + "FRIDAY": "Sexta-feira", + "SATURDAY": "sábado", + "TYPING": "digitando...", + "IS_TYPING": "está digitando...", + "CLOSE": "Fechar", + "ENTER_GROUP_NAME": "Insira o nome do grupo", + "ADD_MEMBERS": "Adicionar membros", + "SEND_MESSAGE": "Enviar mensagem", + "UNBLOCK_USER": "Desbloquear usuário", + "BLOCK_USER": "Bloquear usuário", + "DELETE_AND_EXIT": "Excluir e sair", + "LEAVE_GROUP": "Sair do grupo", + "CREATE_GROUP": "Criar grupo", + "SHARED_MEDIA": "Mídia compartilhada", + "VIDEO_CALL": "Chamada de vídeo", + "AUDIO_CALL": "Chamada de áudio", + "LOADING": "Carregando...", + "REPLY": "resposta", + "REPLIES": "respostas", + "LAUNCH": "Lançamento", + "SHARED_COLLABORATIVE_DOCUMENT": "compartilhou um documento colaborativo", + "SHARED_COLLABORATIVE_WHITEBOARD": "compartilhou um quadro branco colaborativo", + "CREATED_WHITEBOARD": "Você criou um novo quadro branco colaborativo", + "CREATED_DOCUMENT": "Você criou um novo documento SOMCollaborative", + "PHOTOS": "Fotos", + "VIDEOS": "Vídeos", + "DOCUMENT": "Documento", + "MESSAGE_IS_DELETED": "A mensagem foi excluída", + "THIS_MESSAGE_DELETED": "⚠️ Esta mensagem foi excluída", + "VIEW_ON_YOUTUBE": "Ver no Youtube", + "SEARCH": "Pesquisar", + "NO_USERS_FOUND": "Nenhum usuário encontrado", + "ERROR": "Erro", + "NO_GROUPS_FOUND": "Nenhum grupo encontrado", + "NO_CHATS_FOUND": "Nenhum bate-papo encontrado", + "MEDIA_MESSAGE": "Mensagem de mídia", + "INCOMING_AUDIO_CALL": "Chamada de áudio recebida", + "INCOMING_VIDEO_CALL": "Chamada de vídeo recebida", + "DECLINE": "Declínio", + "ACCEPT": "Aceitar", + "CALL_INITIATED": "Chamada iniciada", + "OUTGOING_AUDIO_CALL": "Chamada de áudio de saída", + "OUTGOING_VIDEO_CALL": "Chamada de vídeo de saída", + "CALL_REJECTED": "Chamada rejeitada", + "REJECTED_CALL": "Chamada rejeitada", + "CALL_ACCEPTED": "Chamada aceita", + "JOINED": "ingressou", + "LEFT_THE_CALL": "deixou a ligação", + "UNANSWERED_AUDIO_CALL": "Chamada de áudio não atendida", + "UNANSWERED_VIDEO_CALL": "Chamada de vídeo não atendida", + "CALL_ENDED": "Chamada encerrada", + "CANCELLED_CALL": "Chamada cancelada", + "CALL_BUSY": "Chamada ocupada", + "CALLING": "Chamando...", + "ADD": "Adicionar", + "NO_BANNED_MEMBERS_FOUND": "Nenhum membro banido foi encontrado", + "BANNED_MEMBERS": "Membros banidos", + "NAME": "Nome", + "SCOPE": "Escopo", + "UNBAN": "Unban", + "SELECT_GROUP_TYPE": "Selecione o tipo de grupo", + "ENTER_GROUP_PASSWORD": "Digite a senha do grupo", + "CREATE": "Criar", + "CREATE_POLL": "Criar enquete", + "QUESTION": "Pergunta", + "ENTER_YOUR_QUESTION": "Insira sua pergunta", + "OPTIONS": "Opções", + "ENTER_YOUR_OPTION": "Insira sua opção", + "ADD_NEW_OPTION": "Adicionar nova opção", + "VIEW_MEMBERS": "Exibir membros", + "DETAILS": "Detalhes", + "NOTIFICATIONS": "Notificações", + "OTHER": "Outros", + "HELP": "Socorro", + "REPORT_PROBLEM": "Relatar um problema", + "GROUP_MEMBERS": "Membros do grupo", + "BAN": "Banir", + "KICK": "Chute", + "PICK_YOUR_EMOJI": "Escolha seu emoji", + "PRIVATE_GROUP": "Grupo privado", + "PROTECTED_GROUP": "Grupo protegido", + "VISIT": "Visita", + "ATTACH": "Anexar", + "ATTACH_FILE": "Anexar arquivo", + "ATTACH_VIDEO": "Anexar vídeo", + "ATTACH_AUDIO": "Anexar áudio", + "ATTACH_IMAGE": "Anexar imagem", + "COLLABORATIVE_DOCUMENT": "Documento colaborativo", + "COLLABORATIVE_WHITEBOARD": "Quadro branco colaborativo", + "COLLABORATE_USING_DOCUMENT": "Colabore usando um documento", + "COLLABORATE_USING_WHITEBOARD": "Colabore usando um quadro branco", + "EMOJI": "Emoji", + "ENTER_YOUR_MESSAGE_HERE": "Digite sua mensagem aqui", + "NO_MESSAGES_FOUND": "Ainda não há mensagens aqui...", + "THREAD": "Tópico", + "ADD_REACTION": "Adicionar reação", + "NO_STICKERS_FOUND": "Nenhum adesivo encontrado", + "REPLY_TO_THREAD": "Responder ao tópico", + "REPLY_IN_THREAD": "Responder no tópico", + "DELETE_MESSAGE": "Excluir mensagem", + "EDIT_MESSAGE": "Editar mensagem", + "OWNER": "Proprietário", + "CHANGE_SCOPE": "Escopo da alteração", + "STICKER": "Adesivo", + "LAST_ACTIVE_AT": "Ativo pela última vez em", + "VOICE_CALL": "Chamada de voz", + "VIEW_DETAIL": "Exibir detalhes", + "VOTES": "vota", + "VOTE": "votar", + "NO_VOTE": "Sem voto", + "REACTED": "reagiu", + "ADDED": "adicionada", + "UNBANNED": "desbanido", + "MADE": "fez", + "UNANSWERED_CALL": "Chamada não atendida", + "MISSED_AUDIO_CALL": "Chamada de áudio perdida", + "ENTER_YOUR_PASSWORD": "Digite sua senha", + "DOCS": "Documentos", + "NO_RECORDS_FOUND": "Nenhum registro encontrado", + "LIVE_REACTION": "Reação ao vivo", + "SMILEY_PEOPLE": "Smileys e pessoas", + "ANIMALES_NATURE": "Animais e natureza", + "FOOD_DRINK": "Comida e bebida", + "ACTIVITY": "Atividade", + "TRAVEL_PLACES": "Viagens e lugares", + "OBJECTS": "Objetos", + "SYMBOLS": "Símbolos", + "FLAGS": "Bandeiras", + "SENT": "Enviado", + "SEEN": "Visto", + "DELIVERED": "Entregue", + "TRANSLATE_MESSAGE": "Traduzir mensagem", + "LEFT": "esquerdo", + "KICKED": "chutado", + "BANNED": "banido", + "NEW_MESSAGES": "novas mensagens", + "NEW_MESSAGE": "nova mensagem", + "JUMP": "Saltar", + "SELECT_VIDEO_SOURCE": "Selecione a fonte de vídeo", + "SELECT_INPUT_AUDIO_SOURCE": "Selecione a fonte de áudio de entrada", + "SELECT_OUTPUT_AUDIO_SOURCE": "Selecione a fonte de áudio de saída", + "INITIATED_GROUP_CALL": "iniciou uma chamada em grupo", + "YOU_INITIATED_GROUP_CALL": "Você iniciou uma chamada em grupo", + "IGNORE": "Ignora", + "ON_ANOTHER_CALL": "está em outra ligação", + "CREATING": "Criando", + "AVATAR": "Avatar", + "ONGOING_CALL": "Chamada em andamento", + "YOU_ALREADY_ONGOING_CALL": "Você já está em uma chamada em andamento", + "RESIZE": "Redimensionar", + "SETTINGS": "Configurações", + "ACTIONS": "Ações", + "VIEW_PROFILE": "Exibir perfil", + "SEND_MESSAGE_IN_PRIVATE": "Enviar mensagem de forma privada", + "DELETE": "Excluir", + "DELETE_CONFIRM": "Tem certeza de que deseja excluir?", + "CANCEL": "Cancelar", + "LEAVE_CONFIRM": "Tem certeza de que quer sair do grupo?", + "TRANSFER_CONFIRM": "Você é o proprietário do grupo; transfira a propriedade para um membro antes de sair do grupo", + "ADDING": "Adicionando...", + "TRANSFER": "Transferência", + "TRANSFERRING": "Transferindo", + "YES": "sim", + "NO": "Não", + "SOMETHING_WRONG": "Algo deu errado; tente novamente.", + "INVALID_GROUP_NAME": "Insira um nome válido para o grupo e tente novamente", + "INVALID_PASSWORD": "Insira uma senha válida para o grupo e tente novamente", + "INVALID_GROUP_TYPE": "Insira um tipo válido para o grupo e tente novamente", + "WRONG_PASSWORD": "Digite a senha correta e tente novamente", + "INVALID_POLL_QUESTION": "Insira a pergunta necessária antes de criar uma enquete", + "INVALID_POLL_OPTION": "Insira a resposta necessária antes de criar uma enquete", + "SAME_LANGUAGE_MESSAGE": "O idioma selecionado para tradução é semelhante ao idioma da mensagem original", + "LEAVE": "Sair", + "CUSTOM_MESSAGE_LOCATION": "📍 Localização", + "IN_A_THREAD": "Em um tópico ⤵", + "CALLS": "Chamadas", + "OFFLINE": "Off-line", + "YOU": "Você", + "PRIVACY": "Privacidade", + "BLOCKED_USERS": "Usuários bloqueados", + "YOU'VE_BLOCKED": "Você bloqueou", + "NO_PHOTOS": "Sem fotos", + "NO_VIDEOS": "Sem vídeos", + "NO_DOCUMENTS": "Sem documentos", + "GENERATING_REPLIES": "Gerando respostas", + "JOIN": "Unir", + "DELETE_CONFIRM_MESSAGE": "Você gostaria de excluir esta conversa? Essa conversa será excluída de todos os seus dispositivos.", + "CHAT_ERROR_MESSAGE": "Não consigo carregar bate-papos. Por favor, tente novamente", + "TRY_AGAIN": "TENTE NOVAMENTE", + "CONFIRM": "Confirme", + "UNSAFE_CONTENT": "Conteúdo inseguro", + "UNSAFE_CONFIRMATION": "Você certamente deseja ver esse conteúdo inseguro", + "AUDIO_FILE": "Arquivo de áudio", + "SHARED_FILE": "Arquivo compartilhado", + "OPEN_DOCUMENT": "DOCUMENTO ABERTO", + "OPEN_WHITEBOARD": "QUADRO BRANCO ABERTO", + "PEOPLE_VOTED": "pessoas votaram", + "ADD_TO_CHAT": "Adicionar ao chat", + "TAKE_PHOTO": "Tirar uma foto", + "SET_THE_ANSWERS": "DEFINA AS RESPOSTAS", + "ADD_ANOTHER_ANSWER": "Adicionar outra resposta", + "ANSWER": "Resposta", + "ERROR_GROUP_CREATE": "Não é possível criar um grupo", + "TRY_AGAIN_LATER": "Tente novamente mais tarde", + "CONTINUE": "Continuar", + "GROUP_NAME_MAX": "Máximo de 25 caracteres permitidos no nome do grupo", + "GROUP_PASSWORD_BLANK": "Por favor, insira a senha", + "PASSWORD_MAX": "Máximo de 16 caracteres permitidos na senha do grupo", + "GROUP_PASSWORD": "Senha do grupo", + "INCORRECT_PASSWORD": "Senha incorreta", + "OPEN_WHITEBOARD_TO_DRAW": "Abra o quadro branco para desenhar juntos", + "CALL_HISTORY": "Histórico de chamadas", + "NO_CALL_HISTORY": "Nenhuma chamada foi feita ainda", + "CALL_DETAILS": "Detalhes da chamada", + "CONFERENCE_CALL": "teleconferência", + "NEW_GROUP": "Novo grupo", + "TRANSFER_OWNERSHIP": "Transferir propriedade", + "COPY_MESSAGE": "Copiar", + "SHARE": "Compartilhar", + "OPEN_DOCUMENT_TO_DRAW": "Abra o documento para desenhar em conjunto", + "INFORMATION": "Informações da mensagem", + "FORWARD": "Avançar", + "COPY_TEXT": "Copiar texto", + "TEXT": "Texto", + "INCOMING_CALL": "Chamada recebida", + "OUTGOING_CALL": "Chamada de saída", + "MISSED_CALL": "Chamada perdida", + "GROUP_NAME_BLANK": "O nome do grupo não deve estar vazio", + "GROUP_TYPE_BLANK": "O tipo de grupo Não é possível está vazio", + "POLL_QUESTION_BLANK": "A pergunta canauta está vazia", + "POLL_OPTION_BLANK": "A opção Não é possível está vazia", + "NEW_CONVERSATION": "Novo bate-papo", + "FORWARDING": "Enviando mensagens...", + "READ": "Leia", + "No_RECIPIENT": "Nenhum destinatário", + "RECEIPT_INFORMATION": "Informações sobre o recibo", + "MESSAGE": "Mensagem", + "GENERATING_SUMMARY": "Gerando resumo", + "CONVERSATION_SUMMARY": "Resumo da conversa", + "GENERATE_SUMMARY": "Gere um resumo", + "COMETCHAT_BOT_FIRST_MESSAGE": "Como posso te ajudar com essa conversa? Por favor, me faça uma pergunta e eu vou te aconselhar 🙂", + "COMETCHAT_ASK_BOT": "Pergunte", + "COMETCHAT_ASK_AI_BOT": "Pergunte aos AI Bots", + "COMETCHAT_ASK_BOT_SUBTITLE": "Bot de IA", + "FORM_COMPLETION_MESSAGE": "Obrigado por preencher o formulário.", + "FORM": "Formulário", + "CARD": "Cartão", + "RECORDINGS": "Gravações", + "PARTICIPANTS": "Participantes", + "NO_PARTICIPANTS": "Sem participantes", + "NO_RECORDINGS": "Sem gravações", + "CALL_LOGS": "Registros de chamadas", + "CANCELLED_AUDIO_CALL": "Chamada de áudio cancelada", + "CANCELLED_VIDEO_CALL": "Chamada de vídeo cancelada", + "MEET_WITH": "Reúna-se com", + "MIN_MEETING": "reunião principal", + "MORE_TIMES": "Mais vezes", + "SELECT_DAY": "Selecione um dia", + "SELECT_TIME": "Selecione um horário", + "NO_TIME_SLOT_AVAILABLE": "Não há horário disponível nesta data. Por favor, tente outra data.", + "BOOK_NEW_SLOT": "Reserve um novo slot", + "TRY_AGAIN_CAMEL": "Tente novamente", + "TIME_SLOT_UNAVAILABLE": "O intervalo de tempo não está mais disponível. Por favor, escolha outro.", + "MEETING_SCHEDULER": "Agendador de reuniões", + "MIN": "mi" +} diff --git a/src/shared/resources/CometChatLocalize/resources/ru/translation.json b/src/shared/resources/CometChatLocalize/resources/ru/translation.json index fc88987..4fb02f9 100644 --- a/src/shared/resources/CometChatLocalize/resources/ru/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/ru/translation.json @@ -1,292 +1,302 @@ { - "USERS": "Пользователи", - "CHATS": "Чаты", - "GROUPS": "Группы", - "MORE": "Еще", - "MESSAGE_IMAGE": "📷 Изображение", - "MESSAGE_FILE": "📁 Файл", - "MESSAGE_VIDEO": "📹 Видео", - "MESSAGE_AUDIO": "🎵 Аудио", - "CUSTOM_MESSAGE": "У вас есть сообщение", - "SUGGEST_A_REPLY":"Предложите ответ", - "GENERATIONG_ICEBREAKER":"Генерация ледоколов", - "MISSED_VOICE_CALL": "Пропущенный голосовой вызов", - "MISSED_VIDEO_CALL": "Пропущенный видеовызов", - "CUSTOM_MESSAGE_POLL": "📊 Опрос", - "CUSTOM_MESSAGE_STICKER": "💟 Стикер", - "CUSTOM_MESSAGE_DOCUMENT": "📃 Документ", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 Доска", - "ONLINE": "В сети", - "GENERATING_REPLIES":"Генерация ответов", - "ADMINISTRATOR": "Администратор", - "NO_REPLIES_FOUND":"Ответов не найдено", - "MODERATOR": "Модератор", - "PARTICIPANT": "Участник", - "PUBLIC": "Публичный", - "PRIVATE": "Частный", - "PASSWORD_PROTECTED": "Защищено паролем", - "PRIVACY_AND_SECURITY": "Конфиденциальность и безопасность", - "PREFERENCES": "Предпочтения", - "MEMBERS": "Участники", - "TODAY": "Сегодня", - "YESTERDAY": "Вчера", - "SUNDAY": "Воскресенье", - "MONDAY": "Понедельник", - "TUESDAY": "Вторник", - "WEDNESDAY": "Среда", - "THURSDAY": "Четверг", - "FRIDAY": "Пятница", - "SATURDAY": "Суббота", - "TYPING": "набор...", - "IS_TYPING": "набирает...", - "CLOSE": "Закрыть", - "ENTER_GROUP_NAME": "Введите название группы", - "ADD_MEMBERS": "Добавить участников", - "SEND_MESSAGE": "Отправить сообщение", - "UNBLOCK_USER": "Разблокировать пользователя", - "BLOCK_USER": "Заблокировать пользователя", - "DELETE_AND_EXIT": "Удалить и выйти", - "LEAVE_GROUP": "Покинуть группу", - "CREATE_GROUP": "Создать группу", - "SHARED_MEDIA": "Общие медиафайлы", - "VIDEO_CALL": "Видеозвонок", - "AUDIO_CALL": "Аудиовызов", - "LOADING": "Загрузка...", - "REPLY": "ответить", - "REPLIES": "ответы", - "LAUNCH": "Запуск", - "SHARED_COLLABORATIVE_DOCUMENT": "предоставил совместный доступ к документу", - "SHARED_COLLABORATIVE_WHITEBOARD": "предоставил доступ к совместной доске", - "CREATED_WHITEBOARD": "Вы создали новую интерактивную доску", - "CREATED_DOCUMENT": "Вы создали новый совместный документ", - "PHOTOS": "Фотографии", - "VIDEOS": "Видео", - "DOCUMENT": "Документ", - "YOU_DELETED_THIS_MESSAGE": "⚠️ Вы удалили это сообщение", - "THIS_MESSAGE_DELETED": "⚠️ Это сообщение удалено", - "VIEW_ON_YOUTUBE": "Посмотреть на Youtube", - "SEARCH": "Поиск", - "NO_USERS_FOUND": "Пользователи не найдены", - "ERROR": "Ошибка", - "NO_GROUPS_FOUND": "Группы не найдены", - "NO_CHATS_FOUND": "Чатов не найдено", - "MEDIA_MESSAGE": "Сообщение СМИ", - "INCOMING_AUDIO_CALL": "Входящий аудиовызов", - "INCOMING_VIDEO_CALL": "Входящий видеозвонок", - "DECLINE": "Отклонить", - "ACCEPT": "Принять", - "CALL_INITIATED": "Вызов инициирован", - "OUTGOING_AUDIO_CALL": "Исходящий аудиовызов", - "OUTGOING_VIDEO_CALL": "Исходящий видеозвонок", - "CALL_REJECTED": "Вызов отклонен", - "REJECTED_CALL": "отклоненный вызов", - "CALL_ACCEPTED": "Вызов принят", - "JOINED": "присоединился", - "LEFT_THE_CALL": "покинул вызов", - "UNANSWERED_AUDIO_CALL": "Аудиовызов без ответа", - "UNANSWERED_VIDEO_CALL": "Видеозвонок без ответа", - "CALL_ENDED": "Звонок завершен", - "CANCELLED_CALL": "Отмененный звонок", - "CALL_BUSY": "Вызов занят", - "CALLING": "Звонок...", - "ADD": "Добавить", - "NO_BANNED_MEMBERS_FOUND": "Заблокированных участников не найдено", - "BANNED_MEMBERS": "Заблокированные участники", - "NAME": "Имя", - "SCOPE": "Сфера действия", - "UNBAN": "Разблокировать", - "SELECT_GROUP_TYPE": "Выбрать тип группы", - "ENTER_GROUP_PASSWORD": "Введите пароль группы", - "CREATE": "Создать", - "CREATE_POLL": "Создать опрос", - "QUESTION": "Вопрос", - "ENTER_YOUR_QUESTION": "Введите свой вопрос", - "OPTIONS": "Параметры", - "ENTER_YOUR_OPTION": "Введите свой вариант", - "ADD_NEW_OPTION": "Добавить новый вариант", - "VIEW_MEMBERS": "Просмотр участников", - "DETAILS": "Подробности", - "NOTIFICATIONS": "Уведомления", - "OTHER": "Другое", - "HELP": "Помощь", - "REPORT_PROBLEM": "Сообщить о проблеме", - "GROUP_MEMBERS": "Участники группы", - "BAN": "Заблокировать", - "KICK": "Выгнать", - "PICK_YOUR_EMOJI": "Выберите свой emoji", - "PRIVATE_GROUP": "Частная группа", - "PROTECTED_GROUP": "Защищенная группа", - "VISIT": "Посещение", - "ATTACH": "Прикрепить", - "ATTACH_FILE": "Прикрепить файл", - "ATTACH_VIDEO": "Прикрепить видео", - "ATTACH_AUDIO": "Прикрепить аудио", - "ATTACH_IMAGE": "Прикрепить изображение", - "COLLABORATE_USING_DOCUMENT": "Совместная работа с помощью документа", - "COLLABORATE_USING_WHITEBOARD": "Совместная работа с помощью доски", - "EMOJI": "Emoji", - "ENTER_YOUR_MESSAGE_HERE": "Введите свое сообщение здесь", - "NO_MESSAGES_FOUND": "Сообщения не найдены", - "THREAD": "Ветка", - "COLLABORATIVE_DOCUMENT": "Общий документ", - "COLLABORATIVE_WHITEBOARD": "Доска для совместной работы", - "ADD_REACTION": "Добавить реакцию", - "NO_STICKERS_FOUND": "Наклейки не найдены", - "REPLY_TO_THREAD": "Ответить на ветку", - "REPLY_IN_THREAD": "Ответить в ветке", - "DELETE_MESSAGE": "Удалить сообщение", - "EDIT_MESSAGE": "Редактировать сообщение", - "OWNER": "Владелец", - "CHANGE_SCOPE": "Изменить область действия", - "STICKER": "Наклейка", - "LAST_ACTIVE_AT": "Последняя активность в", - "VOICE_CALL": "Голосовой вызов", - "VIEW_DETAIL": "Подробнее", - "VOTES": "голоса", - "VOTE": "проголосовать", - "NO_VOTE": "Нет голоса", - "REACTED": "отреагировал", - "ADDED": "добавлено", - "UNBANNED": "разблокирован", - "MADE": "сделано", - "UNANSWERED_CALL": "Звонок без ответа", - "MISSED_AUDIO_CALL": "Пропущенный аудиовызов", - "ENTER_YOUR_PASSWORD": "Введите свой пароль", - "DOCS": "Документы", - "NO_RECORDS_FOUND": "Записей не найдено", - "LIVE_REACTION": "Реакция", - "SMILEY_PEOPLE": "Смайлы и люди", - "ANIMALES_NATURE": "Животные и природа", - "FOOD_DRINK": "Еда и напитки", - "ACTIVITY": "Активность", - "TRAVEL_PLACES": "Путешествия и места", - "OBJECTS": "Объекты", - "SYMBOLS": "Символы", - "FLAGS": "Флаги", - "SENT": "Отправлено", - "SEEN": "Просмотрено", - "DELIVERED": "Доставлено", - "TRANSLATE_MESSAGE": "Перевести сообщение", - "LEFT": "слева", - "KICKED": "выгнан", - "BANNED": "заблокирован", - "NEW_MESSAGES": "новые сообщения", - "NEW_MESSAGE": "новое сообщение", - "JUMP": "Прыгать", - "SELECT_VIDEO_SOURCE": "Выбрать источник видео", - "SELECT_INPUT_AUDIO_SOURCE": "Выбрать источник входного аудиосигнала", - "SELECT_OUTPUT_AUDIO_SOURCE": "Выбрать источник звука для вывода", - "INITIATED_GROUP_CALL": "инициировал групповой вызов", - "YOU_INITIATED_GROUP_CALL": "Вы инициировали групповой вызов", - "IGNORE": "Игнорировать", - "ON_ANOTHER_CALL": "на другом вызове", - "CREATING": "Создание", - "AVATAR": "Аватар", - "GROUP_NAME_BLANK": "Имя группы не может быть пустым", - "GROUP_TYPE_BLANK": "Тип группы не может быть пустым", - "GROUP_PASSWORD_BLANK": "Пароль группы не может быть пустым", - "POLL_QUESTION_BLANK": "Вопрос не может быть пустым", - "POLL_OPTION_BLANK": "Параметр не может быть пустым", - "ONGOING_CALL": "Текущий звонок", - "YOU_ALREADY_ONGOING_CALL": "Вы уже находитесь в постоянном вызове", - "RESIZE": "Изменить размер", - "SETTINGS": "Настройки", - "ACTIONS": "Действия", - "VIEW_PROFILE": "Просмотреть профиль", - "SEND_MESSAGE_IN_PRIVATE": "Отправить сообщение в частном порядке", - "DELETE": "Удалить", - "DELETE_CONFIRM": "Вы действительно хотите удалить?", - "CANCEL": "Отменить", - "LEAVE_CONFIRM": "Вы уверены, что хотите покинуть группу?", - "TRANSFER_CONFIRM": "Вы являетесь владельцем группы, пожалуйста, передайте право собственности участнику перед выходом из группы", - "ADDING": "Добавление...", - "TRANSFER": "Трансфер", - "TRANSFERRING": "Перенос", - "YES": "Да", - "NO": "Нет", - "SOMETHING_WRONG": "Что-то пошло не так, повторите попытку", - "INVALID_GROUP_NAME": "Введите действительное имя группы и повторите попытку", - "INVALID_GROUP_TYPE": "Введите допустимый тип для группы и повторите попытку", - "INVALID_PASSWORD": "Введите действительный пароль для группы и повторите попытку", - "WRONG_PASSWORD": "Введите правильный пароль и повторите попытку", - "INVALID_POLL_QUESTION": "Пожалуйста, введите требуемый вопрос перед созданием опроса", - "INVALID_POLL_OPTION": "Пожалуйста, введите требуемый ответ перед созданием опроса", - "SAME_LANGUAGE_MESSAGE": "Выбранный язык перевода аналогичен языку оригинального сообщения", - "LEAVE": "Оставьте", - "CALLS": "Звонки", - "CUSTOM_MESSAGE_LOCATION": "📍 Местоположение", - "OFFLINE": "Не в сети", - "YOU": "Вы", - "PRIVACY": "Конфиденциальность", - "BLOCKED_USERS": "Заблокированные пользователи", - "YOU'VE_BLOCKED": "Вы заблокировали", - "NO_PHOTOS": "Нет фотографий", - "NO_VIDEOS": "Нет видео", - "NO_DOCUMENTS": "Нет документов", - "JOIN": "Присоединиться", - "DELETE_CONFIRM_MESSAGE": "Хотите удалить этот разговор? Этот разговор будет удален со всех ваших устройств.", - "CHAT_ERROR_MESSAGE": "Невозможно загрузить чаты. Пожалуйста, попробуйте еще раз", - "TRY_AGAIN": "ПОПРОБУЙ ЕЩЕ РАЗ", - "CONFIRM": "Подтвердите", - "UNSAFE_CONTENT": "НЕБЕЗОПАСНЫЙ КОНТЕНТ", - "UNSAFE_CONFIRMATION": "ВЫ ДЕЙСТВИТЕЛЬНО ХОТИТЕ УВИДЕТЬ ЭТОТ НЕБЕЗОПАСНЫЙ КОНТЕНТ?", - "AUDIO_FILE": "ЗВУКОВОЙ ФАЙЛ", - "SHARED_FILE": "ОБЩИЙ ФАЙЛ", - "OPEN_DOCUMENT": "ОТКРЫТЫЙ ДОКУМЕНТ", - "OPEN_WHITEBOARD": "ОТКРЫТАЯ БЕЛАЯ ДОСКА", - "PEOPLE_VOTED": "ЛЮДИ ГОЛОСУЮТ", - "ADD_TO_CHAT": "ДОБАВИТЬ В CHA", - "TAKE_PHOTO": "СДЕЛАЙТЕ ФОТОГРАФИЮ", - "SET_THE_ANSWERS": "ЗАДАЙТЕ ОТВЕТЫ", - "ADD_ANOTHER_ANSWER": "ДОБАВИТЬ ЕЩЕ ОДИН ОТВЕТ", - "ANSWER": "ОТВЕТ", - "IN_A_THREAD": "В теме ⤵", - "ERROR_GROUP_CREATE": "Невозможно создать группу", - "TRY_AGAIN_LATER": "Пожалуйста, попробуйте позже", - "CONTINUE": "Продолжить", - "GROUP_NAME_MAX": "В имени группы разрешено использовать не более 25 символов", - "PASSWORD_MAX": "В групповом пароле разрешено использовать не более 16 символов", - "GROUP_PASSWORD": "Групповой пароль", - "INCORRECT_PASSWORD": "Неверный пароль", - "OPEN_WHITEBOARD_TO_DRAW": "Откройте доску, чтобы рисовать вместе", - "CALL_HISTORY": "История звонков", - "NO_CALL_HISTORY": "Звонок пока не поступил", - "CALL_DETAILS": "Сведения о звонке", - "CONFERENCE_CALL": "конференц-звонок", - "NEW_GROUP": "Новая группа", - "TRANSFER_OWNERSHIP": "Передача права собственности", - "COPY_MESSAGE": "Скопировать", - "SHARE": "Поделись", - "OPEN_DOCUMENT_TO_DRAW": "Откройте документ для совместного рисования", - "INFORMATION": "Информация", - "COPY_TEXT": "Скопировать текст", - "FORWARD": "Вперед", - "TEXT": "Текст", - "INCOMING_CALL": "Входящий звонок", - "OUTGOING_CALL": "Исходящий звонок", - "MISSED_CALL": "Пропущенный звонок", - "NEW_CONVERSATION": "Новый чат", - "FORWARDING": "Отправка сообщений..", - "READ": "Прочитайте", - "No_RECIPIENT": "Нет получателя", - "RECEIPT_INFORMATION": "Информация о чеке", - "MESSAGE": "Послание", - "GENERATING_SUMMARY":"Создание сводки", - "CONVERSATION_SUMMARY":"Краткое содержание беседы", - "GENERATE_SUMMARY":"Сгенерируйте сводку", - "COMETCHAT_BOT_FIRST_MESSAGE":"Чем я могу помочь вам в этом разговоре? Пожалуйста, задайте мне вопрос, и я дам вам совет 🙂", - "COMETCHAT_ASK_BOT":"Спросите", - "COMETCHAT_ASK_AI_BOT":"Спросите роботов AI", - "COMETCHAT_ASK_BOT_SUBTITLE":"ИИ-бот", - "FORM_COMPLETION_MESSAGE": "Спасибо за заполнение формы.", - "FORM":"Форма", - "CARD":"Карточка", - "RECORDINGS":"Записи", - "PARTICIPANTS":"Участники", - "NO_PARTICIPANTS":"Нет участников", - "NO_RECORDINGS":"Нет записей", - "CALL_LOGS":"Журналы вызовов", - "CANCELLED_AUDIO_CALL":"Отмененный аудиозвонок", - "CANCELLED_VIDEO_CALL":"Отмененный видеозвонок", - "MICROPHONE_PERMISSION":"Мы используем микрофон для записи аудиосообщений и обмена ими. Перейдите в настройки, чтобы включить доступ к микрофону" -} \ No newline at end of file + "USERS": "Пользователи", + "CHATS": "Чаты", + "GROUPS": "Группы", + "MORE": "Больше", + "MESSAGE_IMAGE": "📷 Изображение", + "MESSAGE_FILE": "📁 Файл", + "MESSAGE_VIDEO": "📹 Видео", + "MESSAGE_AUDIO": "🎵 Аудио", + "CUSTOM_MESSAGE": "У вас есть сообщение", + "MISSED_VOICE_CALL": "Пропущенный голосовой вызов", + "MISSED_VIDEO_CALL": "Пропущенный видеозвонок", + "CUSTOM_MESSAGE_POLL": "📊 Опрос", + "CUSTOM_MESSAGE_STICKER": "💟 Наклейка", + "CUSTOM_MESSAGE_DOCUMENT": "📃 Документ", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 Белая доска", + "ONLINE": "Онлайн", + "ADMINISTRATOR": "Администратор", + "MODERATOR": "ведущий", + "PARTICIPANT": "Участник", + "SUGGEST_A_REPLY": "Предложите ответ", + "GENERATIONG_ICEBREAKER": "Генерация ледоколов", + "PUBLIC": "Общественность", + "NO_REPLIES_FOUND": "Ответов не найдено", + "PRIVATE": "Частное", + "PASSWORD_PROTECTED": "Защищено паролем", + "PRIVACY_AND_SECURITY": "Конфиденциальность и безопасность", + "PREFERENCES": "Предпочтения", + "MEMBERS": "Члены", + "TODAY": "Сегодня", + "YESTERDAY": "Вчера", + "SUNDAY": "Воскресенье", + "MONDAY": "понедельник", + "TUESDAY": "вторник", + "WEDNESDAY": "Среда", + "THURSDAY": "Четверг", + "FRIDAY": "пятница", + "SATURDAY": "суббота", + "TYPING": "набрав...", + "IS_TYPING": "печатает...", + "CLOSE": "Закрыть", + "ENTER_GROUP_NAME": "Введите имя группы", + "ADD_MEMBERS": "Добавить участников", + "SEND_MESSAGE": "Отправить сообщение", + "UNBLOCK_USER": "Разблокировать пользователя", + "BLOCK_USER": "Заблокировать пользователя", + "DELETE_AND_EXIT": "Удалить и выйти", + "LEAVE_GROUP": "Покинуть группу", + "CREATE_GROUP": "Создать группу", + "SHARED_MEDIA": "Общие медиафайлы", + "VIDEO_CALL": "Видеозвонок", + "AUDIO_CALL": "Звуковой звонок", + "LOADING": "Загрузка...", + "REPLY": "ответить", + "REPLIES": "ответы", + "LAUNCH": "Запуск", + "SHARED_COLLABORATIVE_DOCUMENT": "поделился совместным документом", + "SHARED_COLLABORATIVE_WHITEBOARD": "поделился совместной доской", + "CREATED_WHITEBOARD": "Вы создали новую доску для совместной работы", + "CREATED_DOCUMENT": "Вы создали новый документ SOMCollaborative", + "PHOTOS": "Фотографии", + "VIDEOS": "Видеоролики", + "DOCUMENT": "Документ", + "MESSAGE_IS_DELETED": "Сообщение удалено", + "THIS_MESSAGE_DELETED": "⚠️ Это сообщение было удалено", + "VIEW_ON_YOUTUBE": "Смотреть на Youtube", + "SEARCH": "Поиск", + "NO_USERS_FOUND": "Пользователи не найдены", + "ERROR": "Ошибка", + "NO_GROUPS_FOUND": "Группы не найдены", + "NO_CHATS_FOUND": "Чаты не найдены", + "MEDIA_MESSAGE": "Сообщение для СМИ", + "INCOMING_AUDIO_CALL": "Входящий аудиозвонок", + "INCOMING_VIDEO_CALL": "Входящий видеозвонок", + "DECLINE": "Снижение", + "ACCEPT": "Принять", + "CALL_INITIATED": "Вызов инициирован", + "OUTGOING_AUDIO_CALL": "Исходящий аудиозвонок", + "OUTGOING_VIDEO_CALL": "Исходящий видеозвонок", + "CALL_REJECTED": "Вызов отклонен", + "REJECTED_CALL": "Отклоненный вызов", + "CALL_ACCEPTED": "Вызов принят", + "JOINED": "совместный", + "LEFT_THE_CALL": "оставил звонок", + "UNANSWERED_AUDIO_CALL": "Звуковой звонок без ответа", + "UNANSWERED_VIDEO_CALL": "Видеозвонок без ответа", + "CALL_ENDED": "Звонок завершен", + "CANCELLED_CALL": "Отмененный звонок", + "CALL_BUSY": "Звонок занят", + "CALLING": "Звоню...", + "ADD": "Добавить", + "NO_BANNED_MEMBERS_FOUND": "Забаненных участников не найдено", + "BANNED_MEMBERS": "Забаненные участники", + "NAME": "Имя", + "SCOPE": "Область применения", + "UNBAN": "Разбанить", + "SELECT_GROUP_TYPE": "Выберите тип группы", + "ENTER_GROUP_PASSWORD": "Введите групповой пароль", + "CREATE": "Создайте", + "CREATE_POLL": "Создать опрос", + "QUESTION": "Вопрос", + "ENTER_YOUR_QUESTION": "Введите свой вопрос", + "OPTIONS": "Опции", + "ENTER_YOUR_OPTION": "Введите свой вариант", + "ADD_NEW_OPTION": "Добавить новую опцию", + "VIEW_MEMBERS": "Показать участников", + "DETAILS": "Подробности", + "NOTIFICATIONS": "Уведомления", + "OTHER": "Другой", + "HELP": "Помощь", + "REPORT_PROBLEM": "Сообщить о проблеме", + "GROUP_MEMBERS": "Члены группы", + "BAN": "Запрет", + "KICK": "Пинать", + "PICK_YOUR_EMOJI": "Выберите свой смайлик", + "PRIVATE_GROUP": "Частная группа", + "PROTECTED_GROUP": "Защищенная группа", + "VISIT": "Посетите", + "ATTACH": "Прикрепить", + "ATTACH_FILE": "Прикрепить файл", + "ATTACH_VIDEO": "Прикрепите видео", + "ATTACH_AUDIO": "Прикрепите аудио", + "ATTACH_IMAGE": "Прикрепите изображение", + "COLLABORATIVE_DOCUMENT": "Совместный документ", + "COLLABORATIVE_WHITEBOARD": "Белая доска для совместной работы", + "COLLABORATE_USING_DOCUMENT": "Совместная работа с помощью документа", + "COLLABORATE_USING_WHITEBOARD": "Сотрудничайте с помощью белой доски", + "EMOJI": "смайлик", + "ENTER_YOUR_MESSAGE_HERE": "Введите здесь свое сообщение", + "NO_MESSAGES_FOUND": "Здесь пока нет сообщений...", + "THREAD": "Тема", + "ADD_REACTION": "Добавить реакцию", + "NO_STICKERS_FOUND": "Стикеры не найдены", + "REPLY_TO_THREAD": "Ответить на тему", + "REPLY_IN_THREAD": "Ответ в теме", + "DELETE_MESSAGE": "Удалить сообщение", + "EDIT_MESSAGE": "Изменить сообщение", + "OWNER": "Владелец", + "CHANGE_SCOPE": "Изменить область", + "STICKER": "Наклейка", + "LAST_ACTIVE_AT": "Последнее активное сообщение", + "VOICE_CALL": "Голосовой вызов", + "VIEW_DETAIL": "Показать подробности", + "VOTES": "голосов", + "VOTE": "голосования", + "NO_VOTE": "Нет голосования", + "REACTED": "отреагировал", + "ADDED": "добавил", + "UNBANNED": "разблокирован", + "MADE": "сделал", + "UNANSWERED_CALL": "Звонок без ответа", + "MISSED_AUDIO_CALL": "Пропущенный аудиозвонок", + "ENTER_YOUR_PASSWORD": "Введите свой пароль", + "DOCS": "Документы", + "NO_RECORDS_FOUND": "Записей не найдено", + "LIVE_REACTION": "Живая реакция", + "SMILEY_PEOPLE": "Смайлы и люди", + "ANIMALES_NATURE": "Животные и природа", + "FOOD_DRINK": "Еда и напитки", + "ACTIVITY": "Активность", + "TRAVEL_PLACES": "Путешествия и места", + "OBJECTS": "Объекты", + "SYMBOLS": "Символы", + "FLAGS": "Флаги", + "SENT": "Отправлено", + "SEEN": "Увиденное", + "DELIVERED": "Доставлено", + "TRANSLATE_MESSAGE": "Перевести сообщение", + "LEFT": "левый", + "KICKED": "били ногами", + "BANNED": "запретили", + "NEW_MESSAGES": "новые сообщения", + "NEW_MESSAGE": "новое сообщение", + "JUMP": "Прыжок", + "SELECT_VIDEO_SOURCE": "Выберите источник видео", + "SELECT_INPUT_AUDIO_SOURCE": "Выберите источник входного звука", + "SELECT_OUTPUT_AUDIO_SOURCE": "Выберите выходной источник звука", + "INITIATED_GROUP_CALL": "инициировал групповой звонок", + "YOU_INITIATED_GROUP_CALL": "Вы инициировали групповой звонок", + "IGNORE": "Проигнорировать", + "ON_ANOTHER_CALL": "звонит по другому звонку", + "CREATING": "Создание", + "AVATAR": "Аватар", + "ONGOING_CALL": "Постоянный звонок", + "YOU_ALREADY_ONGOING_CALL": "Вы уже на постоянном звонке", + "RESIZE": "Изменить размер", + "SETTINGS": "Настройки", + "ACTIONS": "Действия", + "VIEW_PROFILE": "Показать профиль", + "SEND_MESSAGE_IN_PRIVATE": "Отправить сообщение конфиденциально", + "DELETE": "Удалить", + "DELETE_CONFIRM": "Вы действительно хотите удалить?", + "CANCEL": "Отменить", + "LEAVE_CONFIRM": "Вы уверены, что хотите покинуть группу?", + "TRANSFER_CONFIRM": "Вы являетесь владельцем группы; пожалуйста, передайте право собственности члену перед тем, как покинуть группу", + "ADDING": "Добавление...", + "TRANSFER": "Трансфер", + "TRANSFERRING": "Передача", + "YES": "Да", + "NO": "Нет", + "SOMETHING_WRONG": "Что-то пошло не так. Пожалуйста, попробуйте еще раз.", + "INVALID_GROUP_NAME": "Введите действительное имя группы и повторите попытку", + "INVALID_PASSWORD": "Введите действительный пароль для группы и повторите попытку", + "INVALID_GROUP_TYPE": "Введите правильный тип группы и повторите попытку", + "WRONG_PASSWORD": "Пожалуйста, введите правильный пароль и попробуйте снова", + "INVALID_POLL_QUESTION": "Пожалуйста, введите необходимый вопрос перед созданием опроса", + "INVALID_POLL_OPTION": "Пожалуйста, введите требуемый ответ перед созданием опроса", + "SAME_LANGUAGE_MESSAGE": "Выбранный язык для перевода аналогичен языку оригинального сообщения", + "LEAVE": "Уехать", + "CUSTOM_MESSAGE_LOCATION": "📍 Местонахождение", + "IN_A_THREAD": "В теме ⤵", + "CALLS": "Звонки", + "OFFLINE": "Не в сети", + "YOU": "Ты", + "PRIVACY": "Конфиденциальность", + "BLOCKED_USERS": "Заблокированные пользователи", + "YOU'VE_BLOCKED": "Вы заблокировали", + "NO_PHOTOS": "Нет фотографий", + "NO_VIDEOS": "Нет видео", + "NO_DOCUMENTS": "Нет документов", + "GENERATING_REPLIES": "Генерация ответов", + "JOIN": "Присоединяйтесь", + "DELETE_CONFIRM_MESSAGE": "Хотите удалить этот разговор? Этот разговор будет удален со всех ваших устройств.", + "CHAT_ERROR_MESSAGE": "Невозможно загрузить чаты. Пожалуйста, попробуйте еще раз", + "TRY_AGAIN": "ПОПРОБУЙТЕ ЕЩЕ РАЗ", + "CONFIRM": "Подтвердить", + "UNSAFE_CONTENT": "Небезопасный контент", + "UNSAFE_CONFIRMATION": "Вы действительно хотите увидеть этот небезопасный контент?", + "AUDIO_FILE": "Аудиофайл", + "SHARED_FILE": "Общий файл", + "OPEN_DOCUMENT": "ОТКРЫТЬ ДОКУМЕНТ", + "OPEN_WHITEBOARD": "ОТКРЫТАЯ ДОСКА", + "PEOPLE_VOTED": "люди проголосовали", + "ADD_TO_CHAT": "Добавить в чат", + "TAKE_PHOTO": "Сфотографируй", + "SET_THE_ANSWERS": "ЗАДАЙТЕ ОТВЕТЫ", + "ADD_ANOTHER_ANSWER": "Добавить еще один ответ", + "ANSWER": "Ответ", + "ERROR_GROUP_CREATE": "Невозможно создать группу", + "TRY_AGAIN_LATER": "Пожалуйста, попробуйте позже", + "CONTINUE": "Продолжить", + "GROUP_NAME_MAX": "В названии группы разрешено использовать не более 25 символов", + "GROUP_PASSWORD_BLANK": "Пожалуйста, введите пароль", + "PASSWORD_MAX": "В групповом пароле разрешено не более 16 символов", + "GROUP_PASSWORD": "Групповой пароль", + "INCORRECT_PASSWORD": "Неверный пароль", + "OPEN_WHITEBOARD_TO_DRAW": "Откройте доску, чтобы рисовать вместе", + "CALL_HISTORY": "История звонков", + "NO_CALL_HISTORY": "Пока не было сделано ни одного звонка", + "CALL_DETAILS": "Сведения о звонке", + "CONFERENCE_CALL": "конференц-звонок", + "NEW_GROUP": "Новая группа", + "TRANSFER_OWNERSHIP": "Передача права собственности", + "COPY_MESSAGE": "Копировать", + "SHARE": "Поделись", + "OPEN_DOCUMENT_TO_DRAW": "Откройте документ для совместного рисования", + "INFORMATION": "Информация о сообщении", + "FORWARD": "Вперед", + "COPY_TEXT": "Скопировать текст", + "TEXT": "Текст", + "INCOMING_CALL": "Входящий звонок", + "OUTGOING_CALL": "Исходящий звонок", + "MISSED_CALL": "Пропущенный звонок", + "GROUP_NAME_BLANK": "Имя группы не должно быть пустым", + "GROUP_TYPE_BLANK": "Тип группы Невозможно пуст", + "POLL_QUESTION_BLANK": "Канат вопроса пуст", + "POLL_OPTION_BLANK": "Опция «Невозможно» пуста", + "NEW_CONVERSATION": "Новый чат", + "FORWARDING": "Отправка сообщений...", + "READ": "Прочтите", + "No_RECIPIENT": "Получателя нет", + "RECEIPT_INFORMATION": "Информация о чеке", + "MESSAGE": "Послание", + "GENERATING_SUMMARY": "Создание сводки", + "CONVERSATION_SUMMARY": "Краткое содержание беседы", + "GENERATE_SUMMARY": "Сгенерируйте резюме", + "COMETCHAT_BOT_FIRST_MESSAGE": "Чем я могу помочь вам в этом разговоре? Пожалуйста, задайте мне вопрос, и я вам посоветую 🙂", + "COMETCHAT_ASK_BOT": "Спросите", + "COMETCHAT_ASK_AI_BOT": "Спросите ботов с искусственным", + "COMETCHAT_ASK_BOT_SUBTITLE": "ИИ-бот", + "FORM_COMPLETION_MESSAGE": "Спасибо за заполнение формы.", + "FORM": "Форма", + "CARD": "Карточка", + "RECORDINGS": "Записи", + "PARTICIPANTS": "Участники", + "NO_PARTICIPANTS": "Нет участников", + "NO_RECORDINGS": "Нет записей", + "CALL_LOGS": "Журналы вызовов", + "CANCELLED_AUDIO_CALL": "Отмененный аудиозвонок", + "CANCELLED_VIDEO_CALL": "Отмененный видеозвонок", + "MEET_WITH": "Встретьтесь с", + "MIN_MEETING": "главная встреча", + "MORE_TIMES": "Больше раз", + "SELECT_DAY": "Выберите день", + "SELECT_TIME": "Выберите время", + "NO_TIME_SLOT_AVAILABLE": "Временной интервал на эту дату недоступен. Пожалуйста, попробуйте другую дату.", + "BOOK_NEW_SLOT": "Забронируйте новый слот", + "TRY_AGAIN_CAMEL": "Попробуй еще раз", + "TIME_SLOT_UNAVAILABLE": "Временной интервал больше не доступен. Пожалуйста, выберите другое.", + "MEETING_SCHEDULER": "Планировщик собраний", + "MIN": "миль" +} diff --git a/src/shared/resources/CometChatLocalize/resources/sv/translation.json b/src/shared/resources/CometChatLocalize/resources/sv/translation.json index c6350bf..9088c4d 100644 --- a/src/shared/resources/CometChatLocalize/resources/sv/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/sv/translation.json @@ -1,292 +1,302 @@ { - "USERS": "Användare", - "CHATS": "Chatt", - "GROUPS": "Grupper", - "MORE": "Mer", - "MESSAGE_IMAGE": "📷 Bild", - "MESSAGE_FILE": "📁 Fil", - "MESSAGE_VIDEO": "📹 Video", - "MESSAGE_AUDIO": "🎵 Ljud", - "CUSTOM_MESSAGE": "Du har ett meddelande", - "MISSED_VOICE_CALL": "Missat telefonsamtal", - "MISSED_VIDEO_CALL": "Misset videosamtal", - "CUSTOM_MESSAGE_POLL": "📊 Poll", - "CUSTOM_MESSAGE_STICKER": "💟 Sticker", - "CUSTOM_MESSAGE_DOCUMENT": "📃 Dokument", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 Whiteboard", - "ONLINE": "Online", - "ADMINISTRATOR": "Administrator", - "SUGGEST_A_REPLY":"Föreslå ett svar", - "GENERATIONG_ICEBREAKER":"Generera isbrytare", - "NO_REPLIES_FOUND":"Inga svar hittades", - "MODERATOR": "Moderator", - "PARTICIPANT": "Deltagare", - "GENERATING_REPLIES":"Generera svar", - "PUBLIC": "Publikt", - "PRIVATE": "Privat", - "PASSWORD_PROTECTED": "Lösenordsskyddat", - "PRIVACY_AND_SECURITY": "Sekretess och säkerhet", - "PREFERENCES": "Inställningar", - "MEMBERS": "Medlemmar", - "TODAY": "Idag", - "YESTERDAY": "Igår", - "SUNDAY": "Söndag", - "MONDAY": "Måndag", - "TUESDAY": "Tisdag", - "WEDNESDAY": "Onsdag", - "THURSDAY": "Torsdag", - "FRIDAY": "Fredag", - "SATURDAY": "Lördag", - "TYPING": "skriver...", - "IS_TYPING": "skriver...", - "CLOSE": "Stäng", - "ENTER_GROUP_NAME": "Ange gruppnamn", - "ADD_MEMBERS": "Lägg till medlemmar", - "SEND_MESSAGE": "Skicka meddelande", - "UNBLOCK_USER": "Ta bort blockering från användare", - "BLOCK_USER": "Blockera användare", - "DELETE_AND_EXIT": "Ta bort och stäng", - "LEAVE_GROUP": "Lämna grupp", - "CREATE_GROUP": "Skapa grupp", - "SHARED_MEDIA": "Delade filer", - "VIDEO_CALL": "Videosamtal", - "AUDIO_CALL": "Telefonsamtal", - "LOADING": "Laddar...", - "REPLY": "svara", - "REPLIES": "svar", - "LAUNCH": "Öppna", - "SHARED_COLLABORATIVE_DOCUMENT": "has shared a collaborative document", - "SHARED_COLLABORATIVE_WHITEBOARD": "has shared a collaborative whiteboard", - "CREATED_WHITEBOARD": "You’ve created a new collaborative whiteboard", - "CREATED_DOCUMENT": "You’ve created a new collaborative document", - "PHOTOS": "Bilder", - "VIDEOS": "Videos", - "DOCUMENT": "Dokument", - "YOU_DELETED_THIS_MESSAGE": "⚠️ Du tog bort det här meddelandet", - "THIS_MESSAGE_DELETED": "⚠️ Det här meddelandet är borttaget", - "VIEW_ON_YOUTUBE": "Visa på Youtube", - "SEARCH": "Sök", - "NO_USERS_FOUND": "Inga användare hittades", - "ERROR": "Fel", - "NO_GROUPS_FOUND": "Inga grupper hittades", - "NO_CHATS_FOUND": "Inga chatter hittades", - "MEDIA_MESSAGE": "Filmeddelande", - "INCOMING_AUDIO_CALL": "Inkommande telefonsamtal", - "INCOMING_VIDEO_CALL": "Inkommande videosamtal", - "DECLINE": "Avböj", - "ACCEPT": "Acceptera", - "CALL_INITIATED": "Samtal har påbörjat", - "OUTGOING_AUDIO_CALL": "Utgående telefonsamtal", - "OUTGOING_VIDEO_CALL": "Utgående videosamtal", - "CALL_REJECTED": "Samtalet avböjdes", - "REJECTED_CALL": "rejected call", - "CALL_ACCEPTED": "Call accepted", - "JOINED": "har gått med", - "LEFT_THE_CALL": "lämnade chatten", - "UNANSWERED_AUDIO_CALL": "Obesvarat telefonsamtal", - "UNANSWERED_VIDEO_CALL": "Obesvarat videosamtal", - "CALL_ENDED": "Samtal avslutat", - "CANCELLED_CALL": "Avbrutet samtal", - "CALL_BUSY": "Upptagen i samtal", - "CALLING": "Ringer...", - "ADD": "Lägg till", - "NO_BANNED_MEMBERS_FOUND": "Inga avstängda användare hittades", - "BANNED_MEMBERS": "Avstängda användare", - "NAME": "Namn", - "SCOPE": "Rättigheter", - "UNBAN": "Ta bort avstängningen", - "SELECT_GROUP_TYPE": "Välj grupptyp", - "ENTER_GROUP_PASSWORD": "Ange grupplösenord", - "CREATE": "Skapa", - "CREATE_POLL": "Skapa Poll", - "QUESTION": "Fråga", - "ENTER_YOUR_QUESTION": "Ange din fråga", - "OPTIONS": "Alternativ", - "ENTER_YOUR_OPTION": "Ange dina alternativ", - "ADD_NEW_OPTION": "Lägg till nytt alternativ", - "VIEW_MEMBERS": "Visa medlemmar", - "DETAILS": "Detaljer", - "NOTIFICATIONS": "Notifikationer", - "OTHER": "Övrigt", - "HELP": "Hjälp", - "REPORT_PROBLEM": "Rapportera en bugg", - "GROUP_MEMBERS": "Gruppmedlemmar", - "BAN": "Stäng av", - "KICK": "Kicka", - "PICK_YOUR_EMOJI": "Välj din emoji", - "PRIVATE_GROUP": "Privatgrupp", - "PROTECTED_GROUP": "Skyddad grupp", - "VISIT": "Besök", - "ATTACH": "Bifoga", - "ATTACH_FILE": "Bifoga fil", - "ATTACH_VIDEO": "Bifoga video", - "ATTACH_AUDIO": "Bifoga ljud", - "ATTACH_IMAGE": "Bifoga bild", - "COLLABORATE_USING_DOCUMENT": "Samarbeta på ett dokument", - "COLLABORATE_USING_WHITEBOARD": "Samarbeta i en whiteboard", - "EMOJI": "Emoji", - "ENTER_YOUR_MESSAGE_HERE": "Skriv ditt meddelande här", - "NO_MESSAGES_FOUND": "Inga meddelanden hittades", - "THREAD": "Tråd", - "COLLABORATIVE_DOCUMENT": "Samarbeta på ett dokument", - "COLLABORATIVE_WHITEBOARD": "Samarbeta i en whiteboard", - "ADD_REACTION": "Visa en reaktion", - "NO_STICKERS_FOUND": "Inga stickers hittades", - "REPLY_TO_THREAD": "Svara på tråden", - "REPLY_IN_THREAD": "Svara i tråden", - "DELETE_MESSAGE": "Ta bort meddelande", - "EDIT_MESSAGE": "Ändra meddelande", - "OWNER": "Ägare", - "CHANGE_SCOPE": "Ändra rättigheter", - "STICKER": "Sticker", - "LAST_ACTIVE_AT": "Senast aktiv", - "VOICE_CALL": "Telefonsamtal", - "VIEW_DETAIL": "Visa detaljer", - "VOTES": "Röster", - "VOTE": "Röst", - "NO_VOTE": "Inga röster", - "REACTED": "Reagerade", - "ADDED": "Tillagd", - "UNBANNED": "Avstängning borttagen", - "MADE": "skapad", - "UNANSWERED_CALL": "Obesvarat samtal", - "MISSED_AUDIO_CALL": "Missat ljudsamtal", - "ENTER_YOUR_PASSWORD": "Skriv i ditt lösenord", - "DOCS": "Dokumentation", - "NO_RECORDS_FOUND": "Inget hittades", - "LIVE_REACTION": "Riktig reaktioner", - "SMILEY_PEOPLE": "Smileys & Människor", - "ANIMALES_NATURE": "Djur & Natur", - "FOOD_DRINK": "Mat & Dryck", - "ACTIVITY": "Aktivitet", - "TRAVEL_PLACES": "Resa & Ställen", - "OBJECTS": "Objekt", - "SYMBOLS": "Symboler", - "FLAGS": "Flaggor", - "SENT": "Skickat", - "SEEN": "Läst", - "DELIVERED": "Levererat", - "TRANSLATE_MESSAGE": "Översätt meddelande", - "LEFT": "Vänster", - "KICKED": "Kickad", - "BANNED": "Avstängd", - "NEW_MESSAGES": "nya meddelanden", - "NEW_MESSAGE": "nytt meddelande", - "JUMP": "Hoppa", - "SELECT_VIDEO_SOURCE": "Välj videokälla", - "SELECT_INPUT_AUDIO_SOURCE": "Välj ingående ljudkälla", - "SELECT_OUTPUT_AUDIO_SOURCE": "Välj utgående ljudkälla", - "INITIATED_GROUP_CALL": "har startat ett gruppsamtal", - "YOU_INITIATED_GROUP_CALL": "Du har startat ett gruppsamtal", - "IGNORE": "Ignorera", - "ON_ANOTHER_CALL": "är i ett annat samtal", - "CREATING": "Skapar", - "AVATAR": "Avatar", - "GROUP_NAME_BLANK": "Gruppnamn är obligatoriskt", - "GROUP_TYPE_BLANK": "Grupptyp är obligatoriskt", - "GROUP_PASSWORD_BLANK": "Grupplösenord är obligatoriskt", - "POLL_QUESTION_BLANK": "Fråga är obligatoriskt", - "POLL_OPTION_BLANK": "Alternativ är obligatoriskt", - "ONGOING_CALL": "Pågående samtal", - "YOU_ALREADY_ONGOING_CALL": "Du är redan i ett pågående samtal", - "RESIZE": "Ändra storlek", - "SETTINGS": "Inställningar", - "ACTIONS": "Åtgärder", - "VIEW_PROFILE": "Visa profil", - "SEND_MESSAGE_IN_PRIVATE": "Skicka meddelande privat", - "DELETE": "Ta bort", - "DELETE_CONFIRM": "Är du säker på att du vill radera?", - "CANCEL": "Avbryta", - "LEAVE_CONFIRM": "Är du säker på att du vill lämna gruppen?", - "TRANSFER_CONFIRM": "Du är gruppägare. Överför äganderätten till en medlem innan du lämnar gruppen", - "ADDING": "Lägger till...", - "TRANSFER": "Överföring", - "TRANSFERRING": "Överföra", - "YES": "Javisst", - "NO": "Nej", - "SOMETHING_WRONG": "Något gick fel, snälla försök igen", - "INVALID_GROUP_NAME": "Ange ett giltigt namn för gruppen och försök igen", - "INVALID_GROUP_TYPE": "Ange en giltig typ för gruppen och försök igen", - "INVALID_PASSWORD": "Ange ett giltigt lösenord för gruppen och försök igen", - "WRONG_PASSWORD": "Ange rätt lösenord och försök igen", - "INVALID_POLL_QUESTION": "Ange önskad fråga innan du skapar en enkät", - "INVALID_POLL_OPTION": "Ange det svar som krävs innan du skapar en enkät", - "SAME_LANGUAGE_MESSAGE": "Valt språk för översättning liknar språket i det ursprungliga meddelandet", - "LEAVE": "Lämna", - "CALLS": "Samtal", - "CUSTOM_MESSAGE_LOCATION": "📍Plats", - "OFFLINE": "Offline", - "YOU": "Du", - "PRIVACY": "Säkerhet", - "BLOCKED_USERS": "Blockerade användare", - "YOU'VE_BLOCKED": "Du har blockerats", - "NO_PHOTOS": "Inga bilder", - "NO_VIDEOS": "Inga videos", - "NO_DOCUMENTS": "Inga dokument", - "JOIN": "Anslut", - "DELETE_CONFIRM_MESSAGE": "Vill du ta bort den här konversationen? Den här konversationen kommer att raderas från alla dina enheter.", - "CHAT_ERROR_MESSAGE": "Kan inte ladda chattar. Vänligen försök igen", - "TRY_AGAIN": "FÖRSÖK IGEN", - "CONFIRM": "bekräfta", - "UNSAFE_CONTENT": "OSÄKERT INNEHÅLL", - "UNSAFE_CONFIRMATION": "VILL DU SÄKERT SE DETTA OSÄKRA INNEHÅLL", - "AUDIO_FILE": "AUDIO-FIL", - "SHARED_FILE": "DELAD FIL", - "OPEN_DOCUMENT": "ÖPPNA DOKUMENT", - "OPEN_WHITEBOARD": "ÖPPEN VITBOAR", - "PEOPLE_VOTED": "FOLK RÖSTAR", - "ADD_TO_CHAT": "LÄGG TILL I CHA", - "TAKE_PHOTO": "TA ETT FOTO", - "SET_THE_ANSWERS": "STÄLLA IN SVAREN", - "ADD_ANOTHER_ANSWER": "LÄGG TILL ETT ANNAT SVAR", - "ANSWER": "SVAR", - "IN_A_THREAD": "I en tråd ⤵", - "ERROR_GROUP_CREATE": "Kan inte skapa grupp", - "TRY_AGAIN_LATER": "Försök igen senare", - "CONTINUE": "Fortsätta", - "GROUP_NAME_MAX": "Maximalt 25 charector tillåten i Gruppnamn", - "PASSWORD_MAX": "Maximalt 16 charector tillåts i grupplösenord", - "GROUP_PASSWORD": "Gruppera lösenord", - "INCORRECT_PASSWORD": "Felaktigt lösenord", - "OPEN_WHITEBOARD_TO_DRAW": "Öppna whiteboard för att rita ihop", - "CALL_HISTORY": "Samtalshistorik", - "NO_CALL_HISTORY": "Inget samtal har gjorts ännu", - "CALL_DETAILS": "Uppgifter om samtal", - "CONFERENCE_CALL": "telefonkonferens", - "NEW_GROUP": "Ny grupp", - "TRANSFER_OWNERSHIP": "Överför äganderätt", - "COPY_MESSAGE": "Kopiera", - "SHARE": "Dela", - "OPEN_DOCUMENT_TO_DRAW": "Öppna dokument för att rita ihop", - "INFORMATION": "Information", - "COPY_TEXT": "Kopiera text", - "FORWARD": "Framåt", - "TEXT": "Text", - "INCOMING_CALL": "Inkommande samtal", - "OUTGOING_CALL": "Utgående samtal", - "MISSED_CALL": "Missat samtal", - "NEW_CONVERSATION": "Ny chatt", - "FORWARDING": "Skickar meddelanden..", - "READ": "Läs", - "No_RECIPIENT": "Ingen mottagare", - "RECEIPT_INFORMATION": "Information om kvitto", - "MESSAGE": "Meddelande", - "GENERATING_SUMMARY":"Generera sammanfattning", - "CONVERSATION_SUMMARY":"Konversationssammanfattning", - "GENERATE_SUMMARY":"Skapa en sammanfattning", - "COMETCHAT_BOT_FIRST_MESSAGE":"Hur kan jag hjälpa dig med det här samtalet? Ställ en fråga så ska jag ge dig råd 🙂", - "COMETCHAT_ASK_BOT":"Fråga", - "COMETCHAT_ASK_AI_BOT":"Fråga AI-bots", - "COMETCHAT_ASK_BOT_SUBTITLE":"AI Bot", - "FORM_COMPLETION_MESSAGE": "Tack för att du fyller i formuläret.", - "FORM":"Formulär", - "CARD":"Kort", - "RECORDINGS":"Inspelningar", - "PARTICIPANTS":"Deltagare", - "NO_PARTICIPANTS":"Inga deltagare", - "NO_RECORDINGS":"Inga inspelningar", - "CALL_LOGS":"Samtalsloggar", - "CANCELLED_AUDIO_CALL":"Avbrutet ljudsamtal", - "CANCELLED_VIDEO_CALL":"Avbrutet videosamtal", - "MICROPHONE_PERMISSION":"Vi använder mikrofon för att spela in och dela ljudmeddelanden. Gå till inställningar för att aktivera mikrofonåtkomst" -} \ No newline at end of file + "USERS": "Användare", + "CHATS": "Chattar", + "GROUPS": "Grupper", + "MORE": "Mer", + "MESSAGE_IMAGE": "📷 Bild", + "MESSAGE_FILE": "📁 Fil", + "MESSAGE_VIDEO": "📹 Videoklipp", + "MESSAGE_AUDIO": "🎵 Ljud", + "CUSTOM_MESSAGE": "Du har ett meddelande", + "MISSED_VOICE_CALL": "Missat röstsamtal", + "MISSED_VIDEO_CALL": "Missat videosamtal", + "CUSTOM_MESSAGE_POLL": "📊 Omröstning", + "CUSTOM_MESSAGE_STICKER": "💟 Klistermärke", + "CUSTOM_MESSAGE_DOCUMENT": "📃 Dokument", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 Whiteboard", + "ONLINE": "Uppkopplad", + "ADMINISTRATOR": "Administratör", + "MODERATOR": "presentatör", + "PARTICIPANT": "Deltagare", + "SUGGEST_A_REPLY": "Föreslå ett svar", + "GENERATIONG_ICEBREAKER": "Generera isbrytare", + "PUBLIC": "Offentlig", + "NO_REPLIES_FOUND": "Inga svar hittades", + "PRIVATE": "Privat", + "PASSWORD_PROTECTED": "Lösenordsskyddad", + "PRIVACY_AND_SECURITY": "Sekretess och säkerhet", + "PREFERENCES": "Inställningar", + "MEMBERS": "Medlemmar", + "TODAY": "I dag", + "YESTERDAY": "Igår", + "SUNDAY": "söndag", + "MONDAY": "måndag", + "TUESDAY": "tisdag", + "WEDNESDAY": "onsdag", + "THURSDAY": "torsdag", + "FRIDAY": "fredag", + "SATURDAY": "Lördag", + "TYPING": "skriver...", + "IS_TYPING": "skriver...", + "CLOSE": "Stäng", + "ENTER_GROUP_NAME": "Ange gruppnamn", + "ADD_MEMBERS": "Lägg till medlemmar", + "SEND_MESSAGE": "Skicka meddelande", + "UNBLOCK_USER": "Avblockera användare", + "BLOCK_USER": "Blockera användare", + "DELETE_AND_EXIT": "Ta bort och avsluta", + "LEAVE_GROUP": "Lämna gruppen", + "CREATE_GROUP": "Skapa grupp", + "SHARED_MEDIA": "Delade medier", + "VIDEO_CALL": "Videosamtal", + "AUDIO_CALL": "Ljudsamtal", + "LOADING": "Laddar...", + "REPLY": "svar", + "REPLIES": "svar", + "LAUNCH": "Lansera", + "SHARED_COLLABORATIVE_DOCUMENT": "har delat ett samarbetsdokument", + "SHARED_COLLABORATIVE_WHITEBOARD": "har delat en kollaborativ whiteboard", + "CREATED_WHITEBOARD": "Du har skapat en ny kollaborativ whiteboard", + "CREATED_DOCUMENT": "Du har skapat ett nytt SomCollaboration-dokument", + "PHOTOS": "Foton", + "VIDEOS": "Videor", + "DOCUMENT": "Dokument", + "MESSAGE_IS_DELETED": "Meddelandet raderas", + "THIS_MESSAGE_DELETED": "⚠️ Det här meddelandet raderades", + "VIEW_ON_YOUTUBE": "Visa på Youtube", + "SEARCH": "Sök", + "NO_USERS_FOUND": "Inga användare hittades", + "ERROR": "Fel", + "NO_GROUPS_FOUND": "Inga grupper hittades", + "NO_CHATS_FOUND": "Inga chattar hittades", + "MEDIA_MESSAGE": "Mediemeddelande", + "INCOMING_AUDIO_CALL": "Inkommande ljudsamtal", + "INCOMING_VIDEO_CALL": "Inkommande videosamtal", + "DECLINE": "Nedgång", + "ACCEPT": "Acceptera", + "CALL_INITIATED": "Samtal initierat", + "OUTGOING_AUDIO_CALL": "Utgående ljudsamtal", + "OUTGOING_VIDEO_CALL": "Utgående videosamtal", + "CALL_REJECTED": "Samtal avvisat", + "REJECTED_CALL": "Avvisat samtal", + "CALL_ACCEPTED": "Samtal accepterat", + "JOINED": "anslöt sig", + "LEFT_THE_CALL": "lämnade samtalet", + "UNANSWERED_AUDIO_CALL": "Obesvarat ljudsamtal", + "UNANSWERED_VIDEO_CALL": "Obesvarat videosamtal", + "CALL_ENDED": "Samtalet avslutades", + "CANCELLED_CALL": "Avbrutet samtal", + "CALL_BUSY": "Samtal upptagen", + "CALLING": "Ringer...", + "ADD": "Lägg till", + "NO_BANNED_MEMBERS_FOUND": "Inga förbjudna medlemmar hittades", + "BANNED_MEMBERS": "Förbjudna medlemmar", + "NAME": "Namn", + "SCOPE": "Omfattning", + "UNBAN": "Unban", + "SELECT_GROUP_TYPE": "Välj grupptyp", + "ENTER_GROUP_PASSWORD": "Ange grupplösenord", + "CREATE": "Skapa", + "CREATE_POLL": "Skapa omröstning", + "QUESTION": "Fråga", + "ENTER_YOUR_QUESTION": "Fyll i din fråga", + "OPTIONS": "Alternativ", + "ENTER_YOUR_OPTION": "Ange ditt alternativ", + "ADD_NEW_OPTION": "Lägg till nytt alternativ", + "VIEW_MEMBERS": "Visa medlemmar", + "DETAILS": "Detaljer", + "NOTIFICATIONS": "Meddelanden", + "OTHER": "Övrigt", + "HELP": "Hjälp", + "REPORT_PROBLEM": "Rapportera ett problem", + "GROUP_MEMBERS": "Gruppmedlemmar", + "BAN": "Förbud", + "KICK": "Sparka", + "PICK_YOUR_EMOJI": "Välj din emoji", + "PRIVATE_GROUP": "Privat grupp", + "PROTECTED_GROUP": "Skyddad grupp", + "VISIT": "Besök", + "ATTACH": "Bifoga", + "ATTACH_FILE": "Bifoga fil", + "ATTACH_VIDEO": "Bifoga video", + "ATTACH_AUDIO": "Bifoga ljud", + "ATTACH_IMAGE": "Bifoga bild", + "COLLABORATIVE_DOCUMENT": "Samarbetsdokument", + "COLLABORATIVE_WHITEBOARD": "Kollaborativ whiteboard", + "COLLABORATE_USING_DOCUMENT": "Samarbeta med hjälp av ett dokument", + "COLLABORATE_USING_WHITEBOARD": "Samarbeta med hjälp av en whiteboard", + "EMOJI": "Emoji", + "ENTER_YOUR_MESSAGE_HERE": "Skriv ditt meddelande här", + "NO_MESSAGES_FOUND": "Inga meddelanden här än...", + "THREAD": "Tråd", + "ADD_REACTION": "Lägg till reaktion", + "NO_STICKERS_FOUND": "Inga klistermärken hittades", + "REPLY_TO_THREAD": "Svara på tråden", + "REPLY_IN_THREAD": "Svara i tråd", + "DELETE_MESSAGE": "Ta bort meddelande", + "EDIT_MESSAGE": "Redigera meddelande", + "OWNER": "Ägare", + "CHANGE_SCOPE": "Ändra omfattning", + "STICKER": "Klistermärke", + "LAST_ACTIVE_AT": "Senast aktiv kl", + "VOICE_CALL": "Röstsamtal", + "VIEW_DETAIL": "Visa detalj", + "VOTES": "röster", + "VOTE": "rösta", + "NO_VOTE": "Ingen röst", + "REACTED": "reagerade", + "ADDED": "Lagt till", + "UNBANNED": "oförbjudet", + "MADE": "gjord", + "UNANSWERED_CALL": "Obesvarat samtal", + "MISSED_AUDIO_CALL": "Missat ljudsamtal", + "ENTER_YOUR_PASSWORD": "Ange ditt lösenord", + "DOCS": "Dokument", + "NO_RECORDS_FOUND": "Inga poster hittades", + "LIVE_REACTION": "Levande reaktion", + "SMILEY_PEOPLE": "Smileys och människor", + "ANIMALES_NATURE": "Djur & Natur", + "FOOD_DRINK": "Mat & Dryck", + "ACTIVITY": "Aktivitet", + "TRAVEL_PLACES": "Resor & Platser", + "OBJECTS": "Objekt", + "SYMBOLS": "Symboler", + "FLAGS": "flaggor", + "SENT": "Skickat", + "SEEN": "Sett", + "DELIVERED": "Levereras", + "TRANSLATE_MESSAGE": "Översätt meddelande", + "LEFT": "vänster", + "KICKED": "sparkad", + "BANNED": "förbjudna", + "NEW_MESSAGES": "nya meddelanden", + "NEW_MESSAGE": "nytt meddelande", + "JUMP": "Hoppa", + "SELECT_VIDEO_SOURCE": "Välj videokälla", + "SELECT_INPUT_AUDIO_SOURCE": "Välj ingångsljudkälla", + "SELECT_OUTPUT_AUDIO_SOURCE": "Välj ljudkälla för utgång", + "INITIATED_GROUP_CALL": "har initierat ett gruppsamtal", + "YOU_INITIATED_GROUP_CALL": "Du har initierat ett gruppsamtal", + "IGNORE": "Ignorera", + "ON_ANOTHER_CALL": "är på ett annat samtal", + "CREATING": "Skapar", + "AVATAR": "Avatar", + "ONGOING_CALL": "Pågående utlysning", + "YOU_ALREADY_ONGOING_CALL": "Du är redan i ett pågående samtal", + "RESIZE": "Ändra storlek", + "SETTINGS": "Inställningar", + "ACTIONS": "Åtgärder", + "VIEW_PROFILE": "Visa profil", + "SEND_MESSAGE_IN_PRIVATE": "Skicka meddelande privat", + "DELETE": "Ta bort", + "DELETE_CONFIRM": "Är du säker på att du vill ta bort?", + "CANCEL": "Avbryt", + "LEAVE_CONFIRM": "Är du säker på att du vill lämna gruppen?", + "TRANSFER_CONFIRM": "Du är gruppens ägare; överför äganderätten till en medlem innan du lämnar gruppen", + "ADDING": "Lägger till...", + "TRANSFER": "Överföring", + "TRANSFERRING": "Överföring", + "YES": "Javisst", + "NO": "Nej", + "SOMETHING_WRONG": "Något gick fel, försök igen.", + "INVALID_GROUP_NAME": "Ange ett giltigt namn för gruppen och försök igen", + "INVALID_PASSWORD": "Ange ett giltigt lösenord för gruppen och försök igen", + "INVALID_GROUP_TYPE": "Ange en giltig typ för gruppen och försök igen", + "WRONG_PASSWORD": "Ange rätt lösenord och försök igen", + "INVALID_POLL_QUESTION": "Ange önskad fråga innan du skapar en omröstning", + "INVALID_POLL_OPTION": "Ange önskat svar innan du skapar en omröstning", + "SAME_LANGUAGE_MESSAGE": "Valt språk för översättning liknar språket i originalmeddelandet", + "LEAVE": "Lämna", + "CUSTOM_MESSAGE_LOCATION": "📍 Plats", + "IN_A_THREAD": "I en tråd ⤵", + "CALLS": "Samtal", + "OFFLINE": "Offline", + "YOU": "Du", + "PRIVACY": "Integritet", + "BLOCKED_USERS": "Blockerade användare", + "YOU'VE_BLOCKED": "Du har blockerat", + "NO_PHOTOS": "Inga foton", + "NO_VIDEOS": "Inga videor", + "NO_DOCUMENTS": "Inga dokument", + "GENERATING_REPLIES": "Generera svar", + "JOIN": "Gå med", + "DELETE_CONFIRM_MESSAGE": "Vill du ta bort den här konversationen? Den här konversationen kommer att raderas från alla dina enheter.", + "CHAT_ERROR_MESSAGE": "Det går inte att ladda chattar. Vänligen försök igen", + "TRY_AGAIN": "FÖRSÖK IGEN", + "CONFIRM": "Bekräfta", + "UNSAFE_CONTENT": "Osäkert innehåll", + "UNSAFE_CONFIRMATION": "Vill du säkert se detta osäkra innehåll", + "AUDIO_FILE": "Ljudfil", + "SHARED_FILE": "Delad fil", + "OPEN_DOCUMENT": "ÖPPNA DOKUMENT", + "OPEN_WHITEBOARD": "ÖPPEN WHITEBOARD", + "PEOPLE_VOTED": "folk röstade", + "ADD_TO_CHAT": "Lägg till i chatten", + "TAKE_PHOTO": "Ta ett foto", + "SET_THE_ANSWERS": "STÄLL IN SVAREN", + "ADD_ANOTHER_ANSWER": "Lägg till ett annat svar", + "ANSWER": "Svara", + "ERROR_GROUP_CREATE": "Kan inte skapa grupp", + "TRY_AGAIN_LATER": "Försök igen senare", + "CONTINUE": "Fortsätta", + "GROUP_NAME_MAX": "Högst 25 tecken tillåtna i gruppnamn", + "GROUP_PASSWORD_BLANK": "Vänligen ange lösenord", + "PASSWORD_MAX": "Högst 16 tecken tillåtna i grupplösenord", + "GROUP_PASSWORD": "Grupplösenord", + "INCORRECT_PASSWORD": "Felaktigt lösenord", + "OPEN_WHITEBOARD_TO_DRAW": "Öppna whiteboard för att rita tillsammans", + "CALL_HISTORY": "Samtalshistorik", + "NO_CALL_HISTORY": "Inget samtal har gjorts ännu", + "CALL_DETAILS": "Samtalsinformation", + "CONFERENCE_CALL": "telefonkonferens", + "NEW_GROUP": "Ny grupp", + "TRANSFER_OWNERSHIP": "Överför äganderätten", + "COPY_MESSAGE": "Kopiera", + "SHARE": "Dela", + "OPEN_DOCUMENT_TO_DRAW": "Öppna dokument för att rita tillsammans", + "INFORMATION": "Meddelandeinformation", + "FORWARD": "Framåt", + "COPY_TEXT": "Kopiera text", + "TEXT": "Texten", + "INCOMING_CALL": "Inkommande samtal", + "OUTGOING_CALL": "Utgående samtal", + "MISSED_CALL": "Missat samtal", + "GROUP_NAME_BLANK": "Gruppnamnet får inte vara tomt", + "GROUP_TYPE_BLANK": "Grupptyp Kan inte är tom", + "POLL_QUESTION_BLANK": "Fråga canaut är tom", + "POLL_OPTION_BLANK": "Alternativet Kan inte är tomt", + "NEW_CONVERSATION": "Ny chatt", + "FORWARDING": "Skickar meddelanden...", + "READ": "Läsa", + "No_RECIPIENT": "Ingen mottagare", + "RECEIPT_INFORMATION": "Kvittoinformation", + "MESSAGE": "Meddelande", + "GENERATING_SUMMARY": "Generera sammanfattning", + "CONVERSATION_SUMMARY": "Konversationssammanfattning", + "GENERATE_SUMMARY": "Skapa en sammanfattning", + "COMETCHAT_BOT_FIRST_MESSAGE": "Hur kan jag hjälpa dig med det här samtalet? Ställ en fråga så ska jag ge dig råd 🙂", + "COMETCHAT_ASK_BOT": "Fråga", + "COMETCHAT_ASK_AI_BOT": "Fråga AI-bots", + "COMETCHAT_ASK_BOT_SUBTITLE": "AI Bot", + "FORM_COMPLETION_MESSAGE": "Tack för att du fyllde i formuläret.", + "FORM": "Formulär", + "CARD": "Kort", + "RECORDINGS": "Inspelningar", + "PARTICIPANTS": "Deltagare", + "NO_PARTICIPANTS": "Inga deltagare", + "NO_RECORDINGS": "Inga inspelningar", + "CALL_LOGS": "Samtalsloggar", + "CANCELLED_AUDIO_CALL": "Avbrutet ljudsamtal", + "CANCELLED_VIDEO_CALL": "Avbrutet videosamtal", + "MEET_WITH": "Möt med", + "MIN_MEETING": "min möte", + "MORE_TIMES": "Fler gånger", + "SELECT_DAY": "Välj en dag", + "SELECT_TIME": "Välj en tid", + "NO_TIME_SLOT_AVAILABLE": "Ingen tidslucka tillgänglig på detta datum. Försök med ett annat datum.", + "BOOK_NEW_SLOT": "Boka ny spelautomat", + "TRY_AGAIN_CAMEL": "Försök igen", + "TIME_SLOT_UNAVAILABLE": "Tidslucka är inte längre tillgänglig. Vänligen välj en annan.", + "MEETING_SCHEDULER": "Mötesschemaläggare", + "MIN": "mi" +} diff --git a/src/shared/resources/CometChatLocalize/resources/zh-tw/translation.json b/src/shared/resources/CometChatLocalize/resources/zh-tw/translation.json index 5308856..d514f62 100644 --- a/src/shared/resources/CometChatLocalize/resources/zh-tw/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/zh-tw/translation.json @@ -1,292 +1,302 @@ { - "USERS": "使用者", - "CHATS": "聊天", - "GROUPS": "群組", - "MORE": "更多", - "MESSAGE_IMAGE": "📷 圖片", - "MESSAGE_FILE": "📁 檔案", - "MESSAGE_VIDEO": "📹 影片", - "MESSAGE_AUDIO": "🎵 音訊", - "CUSTOM_MESSAGE": "你有一個訊息", - "MISSED_VOICE_CALL": "未接語音通話", - "MISSED_VIDEO_CALL": "未接視訊通話", - "CUSTOM_MESSAGE_POLL": "📊 投票", - "CUSTOM_MESSAGE_STICKER": "💟 貼紙", - "CUSTOM_MESSAGE_DOCUMENT": "📃 文件", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 白板", - "GENERATING_REPLIES":"生成回复", - "ONLINE": "線上", - "NO_REPLIES_FOUND":"找不到回覆", - "ADMINISTRATOR": "管理員", - "MODERATOR": "版主", - "PARTICIPANT": "參與者", - "PUBLIC": "公開", - "PRIVATE": "私人", - "PASSWORD_PROTECTED": "密碼保護", - "PRIVACY_AND_SECURITY": "隱私權與安全性", - "PREFERENCES": "偏好設定", - "MEMBERS": "成員", - "TODAY": "今天", - "YESTERDAY": "昨天", - "TYPING": "輸入...", - "IS_TYPING": "正在輸入...", - "SUGGEST_A_REPLY":"建議回覆", - "GENERATIONG_ICEBREAKER":"生成破冰船", - "CLOSE": "關閉", - "ENTER_GROUP_NAME": "輸入群組名稱", - "ADD_MEMBERS": "加入成員", - "SEND_MESSAGE": "傳送訊息", - "UNBLOCK_USER": "解除封鎖使用者", - "BLOCK_USER": "封鎖使用者", - "DELETE_AND_EXIT": "刪除並結束", - "LEAVE_GROUP": "離開群組", - "CREATE_GROUP": "建立群組", - "SHARED_MEDIA": "共用媒體", - "VIDEO_CALL": "視訊通話", - "AUDIO_CALL": "音訊通話", - "LOADING": "正在載入中...", - "REPLY": "回覆", - "REPLIES": "回覆", - "LAUNCH": "啟動", - "SHARED_COLLABORATIVE_DOCUMENT": "已共用協同合作文件", - "SHARED_COLLABORATIVE_WHITEBOARD": "已共用一個協同合作白板", - "CREATED_WHITEBOARD": "您已建立新的協同合作白板", - "CREATED_DOCUMENT": "您已建立新的協同合作文件", - "PHOTOS": "相片", - "VIDEOS": "影片", - "DOCUMENT": "文件", - "YOU_DELETED_THIS_MESSAGE": "⚠️ 您刪除了此訊息", - "THIS_MESSAGE_DELETED": "⚠️ 此訊息已被刪除", - "VIEW_ON_YOUTUBE": "在 YouTube 上觀看", - "SEARCH": "搜尋", - "NO_USERS_FOUND": "找不到使用者", - "ERROR": "錯誤", - "NO_GROUPS_FOUND": "找不到群組", - "NO_CHATS_FOUND": "找不到對話", - "MEDIA_MESSAGE": "媒體訊息", - "INCOMING_AUDIO_CALL": "音訊通話來電", - "INCOMING_VIDEO_CALL": "視訊通話來電", - "DECLINE": "拒絕", - "ACCEPT": "接受", - "CALL_INITIATED": "通話已啟動", - "OUTGOING_AUDIO_CALL": "撥出音訊通話", - "OUTGOING_VIDEO_CALL": "撥出視訊通話", - "CALL_REJECTED": "電話拒絕", - "REJECTED_CALL": "拒絕的呼叫", - "CALL_ACCEPTED": "已接聽電話", - "JOINED": "加入", - "LEFT_THE_CALL": "已離開通話", - "UNANSWERED_AUDIO_CALL": "未接聽的音訊通話", - "UNANSWERED_VIDEO_CALL": "未接聽的視訊通話", - "CALL_ENDED": "通話結束", - "CANCELLED_CALL": "已取消通話", - "CALL_BUSY": "對方正在忙碌", - "CALLING": "正在撥打...", - "ADD": "新增", - "NO_BANNED_MEMBERS_FOUND": "沒有找到被封鎖的會員", - "BANNED_MEMBERS": "被封鎖的會員", - "NAME": "名稱", - "SCOPE": "範圍", - "UNBAN": "取消封鎖", - "SELECT_GROUP_TYPE": "選取群組類型", - "ENTER_GROUP_PASSWORD": "輸入群組密碼", - "CREATE": "建立", - "CREATE_POLL": "建立投票", - "QUESTION": "問題", - "ENTER_YOUR_QUESTION": "輸入您的問題", - "OPTIONS": "選項", - "ENTER_YOUR_OPTION": "輸入您的選項", - "ADD_NEW_OPTION": "新增選項", - "VIEW_MEMBERS": "檢視成員", - "DETAILS": "詳細資料", - "NOTIFICATIONS": "通知", - "OTHER": "其他", - "HELP": "幫助", - "REPORT_PROBLEM": "報告問題", - "GROUP_MEMBERS": "群組成員", - "BAN": "封鎖", - "KICK": "踢走", - "PICK_YOUR_EMOJI": "挑選您的表情符號", - "PRIVATE_GROUP": "私人群組", - "PROTECTED_GROUP": "密碼保護群組", - "VISIT": "訪問", - "ATTACH": "發送", - "ATTACH_FILE": "發送檔案", - "ATTACH_VIDEO": "發送視訊", - "ATTACH_AUDIO": "發送音訊", - "ATTACH_IMAGE": "發送圖片", - "COLLABORATE_USING_DOCUMENT": "使用協同合作文件", - "COLLABORATE_USING_WHITEBOARD": "使用協同合作白板", - "EMOJI": "表情符號", - "ENTER_YOUR_MESSAGE_HERE": "在此輸入您的訊息", - "NO_MESSAGES_FOUND": "找不到訊息", - "THREAD": "訊息回覆框", - "COLLABORATIVE_DOCUMENT": "協同合作文件", - "COLLABORATIVE_WHITEBOARD": "協同合作白板", - "ADD_REACTION": "加入表情", - "NO_STICKERS_FOUND": "找不到貼紙", - "REPLY_TO_THREAD": "回覆", - "REPLY_IN_THREAD": "在回覆框中回覆", - "DELETE_MESSAGE": "刪除訊息", - "EDIT_MESSAGE": "編輯訊息", - "SUNDAY": "星期日", - "MONDAY": "星期一", - "TUESDAY": "星期二", - "WEDNESDAY": "星期三", - "THURSDAY": "星期四", - "FRIDAY": "星期五", - "SATURDAY": "星期六", - "OWNER": "擁有者", - "CHANGE_SCOPE": "變更範圍", - "STICKER": "貼紙", - "LAST_ACTIVE_AT": "最後上線時間", - "VOICE_CALL": "語音通話", - "VIEW_DETAIL": "檢視詳細資料", - "VOTES": "票", - "VOTE": "投票", - "NO_VOTE": "沒有投票", - "REACTED": "反應", - "ADDED": "添加", - "UNBANNED": "取消封鎖", - "MADE": "製作", - "UNANSWERED_CALL": "未接聽的電話", - "MISSED_AUDIO_CALL": "未接聽的音訊通話", - "ENTER_YOUR_PASSWORD": "輸入您的密碼", - "DOCS": "文件", - "NO_RECORDS_FOUND": "找不到記錄", - "LIVE_REACTION": "即時反應", - "SMILEY_PEOPLE": "笑臉與人物", - "ANIMALES_NATURE": "動物與自然", - "FOOD_DRINK": "食物與飲料", - "ACTIVITY": "活動", - "TRAVEL_PLACES": "旅遊與地點", - "OBJECTS": "物件", - "SYMBOLS": "符號", - "FLAGS": "旗幟", - "SENT": "已送出", - "SEEN": "已讀", - "DELIVERED": "已接收", - "TRANSLATE_MESSAGE": "翻譯訊息", - "LEFT": "已離開", - "KICKED": "踢走", - "BANNED": "封鎖", - "NEW_MESSAGES": "新訊息", - "NEW_MESSAGE": "新訊息", - "JUMP": "跳", - "SELECT_VIDEO_SOURCE": "選擇視訊來源", - "SELECT_INPUT_AUDIO_SOURCE": "選擇輸入音訊來源", - "SELECT_OUTPUT_AUDIO_SOURCE": "選擇輸出音訊來源", - "INITIATED_GROUP_CALL": "已啟動群組通話", - "YOU_INITIATED_GROUP_CALL": "您已啟動群組通話", - "IGNORE": "忽略", - "ON_ANOTHER_CALL": "正在另一個呼叫", - "CREATING": "建立", - "AVATAR": "阿凡達", - "GROUP_NAME_BLANK": "群組名稱不能空白", - "GROUP_TYPE_BLANK": "群組類型不能空白", - "GROUP_PASSWORD_BLANK": "群組密碼不能空白", - "POLL_QUESTION_BLANK": "問題不能空白", - "POLL_OPTION_BLANK": "選項不能空白", - "ONGOING_CALL": "進行中的呼叫", - "YOU_ALREADY_ONGOING_CALL": "您已經在進行中通話", - "RESIZE": "調整大小", - "SETTINGS": "設定", - "ACTIONS": "動作", - "VIEW_PROFILE": "檢視縱斷面", - "SEND_MESSAGE_IN_PRIVATE": "私下傳送訊息", - "DELETE": "刪除", - "DELETE_CONFIRM": "您確定要刪除嗎?", - "CANCEL": "取消", - "LEAVE_CONFIRM": "你確定要離開這個團體嗎", - "TRANSFER_CONFIRM": "您是群組擁有者,請在離開群組前將所有權轉移給會員", - "ADDING": "正在加入...", - "TRANSFER": "轉移", - "TRANSFERRING": "移轉", - "YES": "是", - "NO": "沒有", - "SOMETHING_WRONG": "發生問題,請再試一次", - "INVALID_GROUP_NAME": "請輸入群組的有效名稱,然後再試一次", - "INVALID_GROUP_TYPE": "請輸入群組的有效類型,然後再試一次", - "INVALID_PASSWORD": "請輸入群組的有效密碼,然後再試一次", - "WRONG_PASSWORD": "請輸入正確的密碼,然後再試一次", - "INVALID_POLL_QUESTION": "請在建立投票前輸入必要的問題", - "INVALID_POLL_OPTION": "請在建立投票前輸入必要的答案", - "SAME_LANGUAGE_MESSAGE": "選擇的翻譯語言與原始訊息的語言相似", - "LEAVE": "離開", - "CALLS": "來電", - "CUSTOM_MESSAGE_LOCATION": "📍 位置", - "OFFLINE": "離線", - "YOU": "你", - "PRIVACY": "隱私", - "BLOCKED_USERS": "已封鎖的使用者", - "YOU'VE_BLOCKED": "你已經封鎖了", - "NO_PHOTOS": "沒有相片", - "NO_VIDEOS": "沒有視訊", - "NO_DOCUMENTS": "沒有文件", - "JOIN": "加入", - "DELETE_CONFIRM_MESSAGE": "您要刪除此對話嗎?此對話將從您的所有設備中刪除。", - "CHAT_ERROR_MESSAGE": "無法加載聊天。請再試一次", - "TRY_AGAIN": "再試一次", - "CONFIRM": "確認", - "UNSAFE_CONTENT": "不安全的內容", - "UNSAFE_CONFIRMATION": "你肯定想看到這個不安全的內容", - "AUDIO_FILE": "音頻文件", - "SHARED_FILE": "共享檔案", - "OPEN_DOCUMENT": "開啟文件", - "OPEN_WHITEBOARD": "打開白豬", - "PEOPLE_VOTED": "人們, 投票", - "ADD_TO_CHAT": "添加到茶", - "TAKE_PHOTO": "拍攝相片", - "SET_THE_ANSWERS": "設置答案", - "ADD_ANOTHER_ANSWER": "添加另一個答案", - "ANSWER": "答案", - "IN_A_THREAD": "在一個線程 ⤵", - "ERROR_GROUP_CREATE": "無法建立群組", - "TRY_AGAIN_LATER": "請稍後再試", - "CONTINUE": "繼續", - "GROUP_NAME_MAX": "群組名稱中最多允許 25 個鏈接器", - "PASSWORD_MAX": "群組密碼中最多允許 16 個鏈接器", - "GROUP_PASSWORD": "群組密碼", - "INCORRECT_PASSWORD": "密碼不正確", - "OPEN_WHITEBOARD_TO_DRAW": "打開白板一起繪製", - "CALL_HISTORY": "通話記錄", - "NO_CALL_HISTORY": "尚未撥打電話", - "CALL_DETAILS": "通話詳情", - "CONFERENCE_CALL": "電話會議", - "NEW_GROUP": "新群組", - "TRANSFER_OWNERSHIP": "移轉所有權", - "COPY_MESSAGE": "複製", - "SHARE": "分享", - "OPEN_DOCUMENT_TO_DRAW": "打開文檔以一起繪製", - "INFORMATION": "信息", - "COPY_TEXT": "複製文字", - "FORWARD": "向前", - "TEXT": "文字", - "INCOMING_CALL": "來電", - "OUTGOING_CALL": "撥出電話", - "MISSED_CALL": "未接來電", - "NEW_CONVERSATION": "新聊天", - "FORWARDING": "正在傳送訊息..", - "READ": "閱讀", - "No_RECIPIENT": "沒有收件人", - "RECEIPT_INFORMATION": "收據資料", - "MESSAGE": "留言", - "GENERATING_SUMMARY":"產生摘要", - "CONVERSATION_SUMMARY":"對話摘要", - "GENERATE_SUMMARY":"產生摘要", - "COMETCHAT_BOT_FIRST_MESSAGE":"我該如何幫助您進行這個對話?請問我一個問題,我會給你建議 🙂", - "COMETCHAT_ASK_BOT":"詢問", - "COMETCHAT_ASK_AI_BOT":"詢問 AI 機器人", - "COMETCHAT_ASK_BOT_SUBTITLE":"人工智能機器人", - "FORM_COMPLETION_MESSAGE": "感謝您填寫表格。", - "FORM":"表格", - "CARD":"卡", - "RECORDINGS":"錄音", - "PARTICIPANTS":"參加者", - "NO_PARTICIPANTS":"沒有參加者", - "NO_RECORDINGS":"沒有錄音", - "CALL_LOGS":"通話記錄", - "CANCELLED_AUDIO_CALL":"已取消音訊通話", - "CANCELLED_VIDEO_CALL":"已取消視頻通話", - "MICROPHONE_PERMISSION":"我們使用麥克風錄製和共享音頻消息。前往設定以啟用麥克風存取" -} \ No newline at end of file + "USERS": "使用者", + "CHATS": "聊天", + "GROUPS": "群組", + "MORE": "更多", + "MESSAGE_IMAGE": "📷 圖像", + "MESSAGE_FILE": "📁 文件", + "MESSAGE_VIDEO": "📹 視頻", + "MESSAGE_AUDIO": "🎵 音頻", + "CUSTOM_MESSAGE": "你有一條消息", + "MISSED_VOICE_CALL": "未接的語音通話", + "MISSED_VIDEO_CALL": "未接視訊通話", + "CUSTOM_MESSAGE_POLL": "📊 投票", + "CUSTOM_MESSAGE_STICKER": "💟 貼紙", + "CUSTOM_MESSAGE_DOCUMENT": "📃 文件", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 白板", + "ONLINE": "在線", + "ADMINISTRATOR": "管理員", + "MODERATOR": "主持人", + "PARTICIPANT": "參與者", + "SUGGEST_A_REPLY": "提出回覆", + "GENERATIONG_ICEBREAKER": "產生破冰器", + "PUBLIC": "公眾", + "NO_REPLIES_FOUND": "找不到回覆", + "PRIVATE": "私人", + "PASSWORD_PROTECTED": "密碼保護", + "PRIVACY_AND_SECURITY": "隱私與安全", + "PREFERENCES": "偏好設定", + "MEMBERS": "會員", + "TODAY": "今天", + "YESTERDAY": "昨天", + "SUNDAY": "星期日", + "MONDAY": "星期一", + "TUESDAY": "星期二", + "WEDNESDAY": "星期三", + "THURSDAY": "星期四", + "FRIDAY": "星期五", + "SATURDAY": "星期六", + "TYPING": "輸入...", + "IS_TYPING": "正在打字...", + "CLOSE": "關閉", + "ENTER_GROUP_NAME": "輸入群組名稱", + "ADD_MEMBERS": "新增成員", + "SEND_MESSAGE": "發送訊息", + "UNBLOCK_USER": "解除封鎖使用者", + "BLOCK_USER": "封鎖使用者", + "DELETE_AND_EXIT": "刪除並結束", + "LEAVE_GROUP": "離開組", + "CREATE_GROUP": "建立群組", + "SHARED_MEDIA": "共享媒體", + "VIDEO_CALL": "視訊通話", + "AUDIO_CALL": "音訊通話", + "LOADING": "正在載入...", + "REPLY": "回复", + "REPLIES": "回覆", + "LAUNCH": "啟動", + "SHARED_COLLABORATIVE_DOCUMENT": "已共用協作文件", + "SHARED_COLLABORATIVE_WHITEBOARD": "共享了一個協作白板", + "CREATED_WHITEBOARD": "您已建立新的協作白板", + "CREATED_DOCUMENT": "您已建立新的一個合作文件", + "PHOTOS": "照片", + "VIDEOS": "影片", + "DOCUMENT": "文件", + "MESSAGE_IS_DELETED": "消息已刪除", + "THIS_MESSAGE_DELETED": "⚠️ 此消息已刪除", + "VIEW_ON_YOUTUBE": "在 YouTube 上查看", + "SEARCH": "搜索", + "NO_USERS_FOUND": "找不到使用者", + "ERROR": "錯誤", + "NO_GROUPS_FOUND": "找不到群組", + "NO_CHATS_FOUND": "找不到聊天", + "MEDIA_MESSAGE": "媒體訊息", + "INCOMING_AUDIO_CALL": "傳入音訊通話", + "INCOMING_VIDEO_CALL": "傳入視訊通話", + "DECLINE": "下降", + "ACCEPT": "接受", + "CALL_INITIATED": "通話已啟動", + "OUTGOING_AUDIO_CALL": "外出音訊通話", + "OUTGOING_VIDEO_CALL": "外出視訊通話", + "CALL_REJECTED": "通話拒絕", + "REJECTED_CALL": "拒絕的呼叫", + "CALL_ACCEPTED": "接受電話", + "JOINED": "加入", + "LEFT_THE_CALL": "離開通話", + "UNANSWERED_AUDIO_CALL": "未接聽的音訊通話", + "UNANSWERED_VIDEO_CALL": "未接聽的視頻通話", + "CALL_ENDED": "通話結束", + "CANCELLED_CALL": "已取消通話", + "CALL_BUSY": "打電話忙碌", + "CALLING": "打電話...", + "ADD": "新增", + "NO_BANNED_MEMBERS_FOUND": "找不到被禁用的會員", + "BANNED_MEMBERS": "被禁用的會員", + "NAME": "姓名", + "SCOPE": "範圍", + "UNBAN": "取消禁", + "SELECT_GROUP_TYPE": "選擇群組類型", + "ENTER_GROUP_PASSWORD": "輸入群組密碼", + "CREATE": "建立", + "CREATE_POLL": "建立投票", + "QUESTION": "問題", + "ENTER_YOUR_QUESTION": "輸入您的問題", + "OPTIONS": "選項", + "ENTER_YOUR_OPTION": "輸入您的選項", + "ADD_NEW_OPTION": "新增選項", + "VIEW_MEMBERS": "查看成員", + "DETAILS": "詳情", + "NOTIFICATIONS": "通知", + "OTHER": "其他", + "HELP": "幫助", + "REPORT_PROBLEM": "報告問題", + "GROUP_MEMBERS": "群組成員", + "BAN": "禁令", + "KICK": "踢", + "PICK_YOUR_EMOJI": "選擇您的表情符號", + "PRIVATE_GROUP": "私人團體", + "PROTECTED_GROUP": "受保護群組", + "VISIT": "參觀", + "ATTACH": "附加", + "ATTACH_FILE": "附加檔案", + "ATTACH_VIDEO": "附加影片", + "ATTACH_AUDIO": "附加音訊", + "ATTACH_IMAGE": "附加影像", + "COLLABORATIVE_DOCUMENT": "協作文件", + "COLLABORATIVE_WHITEBOARD": "協作白板", + "COLLABORATE_USING_DOCUMENT": "使用文件進行協作", + "COLLABORATE_USING_WHITEBOARD": "使用白板進行協作", + "EMOJI": "表情符號", + "ENTER_YOUR_MESSAGE_HERE": "在此輸入您的訊息", + "NO_MESSAGES_FOUND": "目前沒有留言...", + "THREAD": "線程", + "ADD_REACTION": "加入反應", + "NO_STICKERS_FOUND": "找不到貼紙", + "REPLY_TO_THREAD": "回覆主題", + "REPLY_IN_THREAD": "在主題中回复", + "DELETE_MESSAGE": "刪除訊息", + "EDIT_MESSAGE": "編輯訊息", + "OWNER": "業主", + "CHANGE_SCOPE": "變更範圍", + "STICKER": "貼紙", + "LAST_ACTIVE_AT": "最後一次活動時間", + "VOICE_CALL": "語音通話", + "VIEW_DETAIL": "查看詳細資料", + "VOTES": "票", + "VOTE": "投票", + "NO_VOTE": "沒有投票", + "REACTED": "反應", + "ADDED": "添加", + "UNBANNED": "未禁止", + "MADE": "製作", + "UNANSWERED_CALL": "未接聽的電話", + "MISSED_AUDIO_CALL": "未接聽音訊通話", + "ENTER_YOUR_PASSWORD": "輸入您的密碼", + "DOCS": "文件", + "NO_RECORDS_FOUND": "找不到任何記錄", + "LIVE_REACTION": "實時反應", + "SMILEY_PEOPLE": "笑臉和人物", + "ANIMALES_NATURE": "動物與自然", + "FOOD_DRINK": "食品和飲料", + "ACTIVITY": "活動", + "TRAVEL_PLACES": "旅遊與地點", + "OBJECTS": "物件", + "SYMBOLS": "符號", + "FLAGS": "旗幟", + "SENT": "已發送", + "SEEN": "已經看到", + "DELIVERED": "已交付", + "TRANSLATE_MESSAGE": "翻譯訊息", + "LEFT": "剩下", + "KICKED": "踢", + "BANNED": "取締", + "NEW_MESSAGES": "新消息", + "NEW_MESSAGE": "新消息", + "JUMP": "跳躍", + "SELECT_VIDEO_SOURCE": "選擇視頻來源", + "SELECT_INPUT_AUDIO_SOURCE": "選擇輸入音頻源", + "SELECT_OUTPUT_AUDIO_SOURCE": "選擇輸出音頻源", + "INITIATED_GROUP_CALL": "已啟動群組通話", + "YOU_INITIATED_GROUP_CALL": "您已啟動群組通話", + "IGNORE": "忽略", + "ON_ANOTHER_CALL": "正在另一次通話", + "CREATING": "建立", + "AVATAR": "阿凡達", + "ONGOING_CALL": "正在進行的電話", + "YOU_ALREADY_ONGOING_CALL": "您已經在進行中的通話", + "RESIZE": "調整大小", + "SETTINGS": "設定", + "ACTIONS": "行動", + "VIEW_PROFILE": "查看個人資料", + "SEND_MESSAGE_IN_PRIVATE": "私下傳送訊息", + "DELETE": "刪除", + "DELETE_CONFIRM": "您確定要刪除嗎?", + "CANCEL": "取消", + "LEAVE_CONFIRM": "你確定要離開團隊嗎?", + "TRANSFER_CONFIRM": "您是群組擁有者,請在離開群組之前將所有權轉移給成員", + "ADDING": "正在加入...", + "TRANSFER": "轉移", + "TRANSFERRING": "轉移", + "YES": "是", + "NO": "否", + "SOMETHING_WRONG": "出現問題;請再試一次。", + "INVALID_GROUP_NAME": "請輸入群組的有效名稱,然後再試一次", + "INVALID_PASSWORD": "請輸入群組的有效密碼,然後再試一次", + "INVALID_GROUP_TYPE": "請輸入有效的群組類型,然後再試一次", + "WRONG_PASSWORD": "請輸入正確的密碼,然後再試一次", + "INVALID_POLL_QUESTION": "請在建立投票前輸入必要的問題", + "INVALID_POLL_OPTION": "請在建立投票前輸入必要答案", + "SAME_LANGUAGE_MESSAGE": "用於翻譯的選擇語言類似於原始消息的語言", + "LEAVE": "離開", + "CUSTOM_MESSAGE_LOCATION": "📍 位置", + "IN_A_THREAD": "在一個線程中 ⤵", + "CALLS": "通話", + "OFFLINE": "離線", + "YOU": "你", + "PRIVACY": "隱私", + "BLOCKED_USERS": "封鎖的使用者", + "YOU'VE_BLOCKED": "您已封鎖", + "NO_PHOTOS": "沒有照片", + "NO_VIDEOS": "沒有影片", + "NO_DOCUMENTS": "沒有文件", + "GENERATING_REPLIES": "產生回覆", + "JOIN": "加入", + "DELETE_CONFIRM_MESSAGE": "您想刪除此對話嗎?此對話將從您的所有設備中刪除。", + "CHAT_ERROR_MESSAGE": "無法載入聊天。請再試一次", + "TRY_AGAIN": "再試一次", + "CONFIRM": "確認", + "UNSAFE_CONTENT": "不安全的內容", + "UNSAFE_CONFIRMATION": "您確定想看到這個不安全的內容嗎", + "AUDIO_FILE": "音頻文件", + "SHARED_FILE": "共用檔案", + "OPEN_DOCUMENT": "開啟文件", + "OPEN_WHITEBOARD": "打開白板", + "PEOPLE_VOTED": "人們投票", + "ADD_TO_CHAT": "加入聊天", + "TAKE_PHOTO": "拍照", + "SET_THE_ANSWERS": "設置答案", + "ADD_ANOTHER_ANSWER": "添加另一個答案", + "ANSWER": "答案", + "ERROR_GROUP_CREATE": "無法建立群組", + "TRY_AGAIN_LATER": "請稍後再試一次", + "CONTINUE": "繼續", + "GROUP_NAME_MAX": "群組名稱中最多允許 25 個字符", + "GROUP_PASSWORD_BLANK": "請輸入密碼", + "PASSWORD_MAX": "群組密碼中最多允許 16 個字符", + "GROUP_PASSWORD": "群組密碼", + "INCORRECT_PASSWORD": "密碼不正確", + "OPEN_WHITEBOARD_TO_DRAW": "打開白板以一起繪製", + "CALL_HISTORY": "通話記錄", + "NO_CALL_HISTORY": "尚未撥打電話", + "CALL_DETAILS": "通話詳情", + "CONFERENCE_CALL": "電話會議", + "NEW_GROUP": "新集團", + "TRANSFER_OWNERSHIP": "轉讓所有權", + "COPY_MESSAGE": "複製", + "SHARE": "分享", + "OPEN_DOCUMENT_TO_DRAW": "打開文件以一起繪製", + "INFORMATION": "訊息資訊", + "FORWARD": "前進", + "COPY_TEXT": "複製文字", + "TEXT": "文字", + "INCOMING_CALL": "來電", + "OUTGOING_CALL": "外撥通話", + "MISSED_CALL": "未接來電", + "GROUP_NAME_BLANK": "群組名稱不應為空", + "GROUP_TYPE_BLANK": "群組類型不能為空", + "POLL_QUESTION_BLANK": "問題名稱為空白", + "POLL_OPTION_BLANK": "選項「不能」為空白", + "NEW_CONVERSATION": "新聊天", + "FORWARDING": "正在傳送訊息...", + "READ": "閱讀", + "No_RECIPIENT": "沒有收件人", + "RECEIPT_INFORMATION": "收據信息", + "MESSAGE": "留言", + "GENERATING_SUMMARY": "產生摘要", + "CONVERSATION_SUMMARY": "對話摘要", + "GENERATE_SUMMARY": "產生摘要", + "COMETCHAT_BOT_FIRST_MESSAGE": "我該如何幫助您進行這個對話?請問我一個問題,我會給你建議 🙂", + "COMETCHAT_ASK_BOT": "詢問", + "COMETCHAT_ASK_AI_BOT": "詢問 AI 機器人", + "COMETCHAT_ASK_BOT_SUBTITLE": "人工智能機器人", + "FORM_COMPLETION_MESSAGE": "感謝您填寫表格。", + "FORM": "表格", + "CARD": "卡", + "RECORDINGS": "錄音", + "PARTICIPANTS": "參加者", + "NO_PARTICIPANTS": "沒有參加者", + "NO_RECORDINGS": "沒有錄音", + "CALL_LOGS": "通話記錄", + "CANCELLED_AUDIO_CALL": "已取消音訊通話", + "CANCELLED_VIDEO_CALL": "已取消視頻通話", + "MEET_WITH": "會面", + "MIN_MEETING": "最小會議", + "MORE_TIMES": "更多次數", + "SELECT_DAY": "選擇一天", + "SELECT_TIME": "選擇時間", + "NO_TIME_SLOT_AVAILABLE": "此日期沒有可用的時段。請嘗試另一個日期。", + "BOOK_NEW_SLOT": "預訂新老虎機", + "TRY_AGAIN_CAMEL": "再試一次", + "TIME_SLOT_UNAVAILABLE": "時段不再可用。請選擇其他。", + "MEETING_SCHEDULER": "會議排程器", + "MIN": "小米" +} diff --git a/src/shared/resources/CometChatLocalize/resources/zh/translation.json b/src/shared/resources/CometChatLocalize/resources/zh/translation.json index a4d7f50..bec423a 100644 --- a/src/shared/resources/CometChatLocalize/resources/zh/translation.json +++ b/src/shared/resources/CometChatLocalize/resources/zh/translation.json @@ -1,293 +1,302 @@ { - "USERS": "用户", - "CHATS": "聊天", - "GROUPS": "群组", - "MORE": "更多", - "MESSAGE_IMAGE": "📷 图片", - "MESSAGE_FILE": "📁 文件", - "MESSAGE_VIDEO": "📹 视频", - "MESSAGE_AUDIO": "🎵 音频", - "CUSTOM_MESSAGE": "你有一个消息", - "NO_REPLIES_FOUND":"未找到回复", - "MISSED_VOICE_CALL": "未接语音通话", - "MISSED_VIDEO_CALL": "未接视频通话", - "CUSTOM_MESSAGE_POLL": "📊 民意调查", - "CUSTOM_MESSAGE_STICKER": "💟 贴纸", - "CUSTOM_MESSAGE_DOCUMENT": "📃 文档", - "CUSTOM_MESSAGE_WHITEBOARD": "📝 白板", - "ONLINE": "在线", - "ADMINISTRATOR": "管理员", - "MODERATOR": "版主", - "PARTICIPANT": "参与者", - "GENERATING_REPLIES":"生成回复", - "PUBLIC": "公开", - "PRIVATE": "私人", - "PASSWORD_PROTECTED": "密码保护", - "PRIVACY_AND_SECURITY": "隐私权与安全性", - "PREFERENCES": "偏好", - "MEMBERS": "会员", - "TODAY": "今天", - "YESTERDAY": "昨天", - "SUNDAY": "周日", - "MONDAY": "周一", - "SUGGEST_A_REPLY":"建议回复", - "GENERATIONG_ICEBREAKER":"生成破冰船", - "TUESDAY": "周二", - "WEDNESDAY": "周三", - "THURSDAY": "周四", - "FRIDAY": "周五", - "SATURDAY": "周六", - "TYPING": "打字...", - "IS_TYPING": "正在打字...", - "CLOSE": "关闭", - "ENTER_GROUP_NAME": "输入组名", - "ADD_MEMBERS": "添加成员", - "SEND_MESSAGE": "发送消息", - "UNBLOCK_USER": "解除封锁使用者", - "BLOCK_USER": "封锁使用者", - "DELETE_AND_EXIT": "删除并退出", - "LEAVE_GROUP": "离开群组", - "CREATE_GROUP": "创建群组", - "SHARED_MEDIA": "共享媒体", - "VIDEO_CALL": "视频通话", - "AUDIO_CALL": "音频通话", - "LOADING": "正在加载...", - "REPLY": "答复", - "REPLIES": "回复", - "LAUNCH": "启动", - "SHARED_COLLABORATIVE_DOCUMENT": "已共用协同合作文件", - "SHARED_COLLABORATIVE_WHITEBOARD": "已共用一个协同合作白板", - "CREATED_WHITEBOARD": "您已建立新的协同合作白板", - "CREATED_DOCUMENT": "您已建立新的协同合作文件", - "PHOTOS": "照片", - "VIDEOS": "视频", - "DOCUMENT": "文档", - "YOU_DELETED_THIS_MESSAGE": "⚠️ 你删除了此消息", - "THIS_MESSAGE_DELETED": "⚠️ 此消息已删除", - "VIEW_ON_YOUTUBE": "在 YouTube 上观看", - "SEARCH": "搜索", - "NO_USERS_FOUND": "找不到用户", - "ERROR": "错误", - "NO_GROUPS_FOUND": "没有找到群组", - "NO_CHATS_FOUND": "没有找到聊天", - "MEDIA_MESSAGE": "媒体消息", - "INCOMING_AUDIO_CALL": "音讯通话来电", - "INCOMING_VIDEO_CALL": "视讯通话来电", - "DECLINE": "拒绝", - "ACCEPT": "接受", - "CALL_INITIATED": "通话已启动", - "OUTGOING_AUDIO_CALL": "拨出音讯通话", - "OUTGOING_VIDEO_CALL": "拨出视讯通话", - "CALL_REJECTED": "通话被拒绝", - "REJECTED_CALL": "被拒绝的通话", - "CALL_ACCEPTED": "已接听电话", - "JOINED": "加入", - "LEFT_THE_CALL": "已离开通话", - "UNANSWERED_AUDIO_CALL": "未接听的音讯通话", - "UNANSWERED_VIDEO_CALL": "未接听的视讯通话", - "CALL_ENDED": "通话结束", - "CANCELLED_CALL": "已取消通话", - "UNANSWERED_CALL": "未接听的电话", - "CALL_BUSY": "对方正在忙碌", - "CALLING": "正在拨打...", - "ADD": "添加", - "NO_BANNED_MEMBERS_FOUND": "没有找到被封锁的会员", - "BANNED_MEMBERS": "被封锁的会员", - "NAME": "姓名", - "SCOPE": "范围", - "UNBAN": "取消封锁", - "SELECT_GROUP_TYPE": "选择群组类型", - "ENTER_GROUP_PASSWORD": "输入群组密码", - "CREATE": "创建", - "CREATE_POLL": "创建投票", - "QUESTION": "问题", - "ENTER_YOUR_QUESTION": "输入你的问题", - "OPTIONS": "选项", - "ENTER_YOUR_OPTION": "输入你的选项", - "ADD_NEW_OPTION": "添加新选项", - "VIEW_MEMBERS": "查看会员", - "DETAILS": "详情", - "NOTIFICATIONS": "通知", - "OTHER": "其他", - "HELP": "帮助", - "REPORT_PROBLEM": "报告问题", - "GROUP_MEMBERS": "群组成员", - "BAN": "封锁", - "KICK": "踢走", - "PICK_YOUR_EMOJI": "选择你的表情符号", - "PRIVATE_GROUP": "私人群组", - "PROTECTED_GROUP": "密码保护群组", - "VISIT": "访问", - "ATTACH": "发送", - "ATTACH_FILE": "发送文件", - "ATTACH_VIDEO": "发送视频", - "ATTACH_AUDIO": "发送音频", - "ATTACH_IMAGE": "发送图片", - "COLLABORATE_USING_DOCUMENT": "使用协同合作文件", - "COLLABORATE_USING_WHITEBOARD": "使用协同合作白板", - "EMOJI": "表情符号", - "ENTER_YOUR_MESSAGE_HERE": "在这里输入你的消息", - "NO_MESSAGES_FOUND": "没有找到消息", - "THREAD": "消息回复框", - "COLLABORATIVE_DOCUMENT": "协作文档", - "COLLABORATIVE_WHITEBOARD": "协作白板", - "ADD_REACTION": "添加表情", - "NO_STICKERS_FOUND": "找不到贴纸", - "REPLY_TO_THREAD": "回复", - "REPLY_IN_THREAD": "在回复框中回复", - "DELETE_MESSAGE": "删除消息", - "EDIT_MESSAGE": "编辑消息", - "OWNER": "拥有者", - "CHANGE_SCOPE": "更改范围", - "STICKER": "贴纸", - "LAST_ACTIVE_AT": "最后在线时间", - "VOICE_CALL": "语音通话", - "VIEW_DETAIL": "查看详情", - "VOTES": "选票", - "VOTE": "投票", - "NO_VOTE": "没有投票", - "REACTED": "反应", - "ADDED": "添加", - "UNBANNED": "未被封锁", - "MADE": "制作", - "CALL_UNANSWERED": "未接听来电", - "MISSED_AUDIO_CALL": "未接听的音讯通话", - "ENTER_YOUR_PASSWORD": "输入您的密码", - "DOCS": "文件", - "NO_RECORDS_FOUND": "没有找到记录", - "LIVE_REACTION": "实时反应", - "SMILEY_PEOPLE": "笑脸与人物", - "ANIMALES_NATURE": "动物与自然", - "FOOD_DRINK": "食物与饮料", - "ACTIVITY": "活动", - "TRAVEL_PLACES": "旅游与地点", - "OBJECTS": "物件", - "SYMBOLS": "符号", - "FLAGS": "旗帜", - "SENT": "已发送", - "SEEN": "已读", - "DELIVERED": "已接收", - "TRANSLATE_MESSAGE": "翻译消息", - "LEFT": "已离开", - "KICKED": "踢走", - "BANNED": "封锁", - "NEW_MESSAGES": "新消息", - "NEW_MESSAGE": "新消息", - "JUMP": "跳", - "SELECT_VIDEO_SOURCE": "选择视频源", - "SELECT_INPUT_AUDIO_SOURCE": "选择输入音频源", - "SELECT_OUTPUT_AUDIO_SOURCE": "选择输出音频源", - "INITIATED_GROUP_CALL": "已发起群组通话", - "YOU_INITIATED_GROUP_CALL": "你已经发起了群组通话", - "IGNORE": "忽略", - "ON_ANOTHER_CALL": "正在另一个电话", - "CREATING": "创建", - "AVATAR": "阿凡达", - "GROUP_NAME_BLANK": "群組名稱不能空白", - "GROUP_TYPE_BLANK": "群組類型不能空白", - "GROUP_PASSWORD_BLANK": "群組密碼不能空白", - "POLL_QUESTION_BLANK": "問題不能空白", - "POLL_OPTION_BLANK": "選項不能空白", - "ONGOING_CALL": "持续的电话", - "YOU_ALREADY_ONGOING_CALL": "你已经在进行中的电话", - "RESIZE": "调整大小", - "SETTINGS": "设置", - "ACTIONS": "动作", - "VIEW_PROFILE": "查看档案", - "SEND_MESSAGE_IN_PRIVATE": "私下发送消息", - "DELETE": "删除", - "DELETE_CONFIRM": "确定要删除吗?", - "CANCEL": "取消", - "LEAVE_CONFIRM": "你确定要离开小组吗?", - "TRANSFER_CONFIRM": "您是群组所有者,请在离开群组之前将所有权转让给成员", - "ADDING": "正在添加...", - "TRANSFER": "转移", - "TRANSFERRING": "转移", - "YES": "是的", - "NO": "不", - "SOMETHING_WRONG": "出错了,请重试", - "INVALID_GROUP_NAME": "请输入组的有效名称,然后重试", - "INVALID_GROUP_TYPE": "请输入组的有效类型,然后重试", - "INVALID_PASSWORD": "请输入组的有效密码然后重试", - "WRONG_PASSWORD": "请输入正确的密码然后重试", - "INVALID_POLL_QUESTION": "请在创建民意调查之前输入必填的问题", - "INVALID_POLL_OPTION": "请在创建民意调查之前输入必填的答案", - "SAME_LANGUAGE_MESSAGE": "选定的翻译语言类似于原始消息的语言", - "LEAVE": "离开", - "CALLS": "来电", - "CUSTOM_MESSAGE_LOCATION": "📍 位置", - "OFFLINE": "离线", - "YOU": "你", - "PRIVACY": "隐私", - "BLOCKED_USERS": "已封锁的用户", - "YOU'VE_BLOCKED": "你已经封锁了", - "NO_PHOTOS": "没有照片", - "NO_VIDEOS": "没有视频", - "NO_DOCUMENTS": "没有文档", - "JOIN": "加入", - "DELETE_CONFIRM_MESSAGE": "你想删除这个对话吗?此对话将从您的所有设备上删除。", - "CHAT_ERROR_MESSAGE": "无法加载聊天。请再试一次", - "TRY_AGAIN": "再试一次", - "CONFIRM": "确认", - "UNSAFE_CONTENT": "不安全内容", - "UNSAFE_CONFIRMATION": "你肯定想看这个不安全的内容吗", - "AUDIO_FILE": "音频文件", - "SHARED_FILE": "共享文件", - "OPEN_DOCUMENT": "打开文档", - "OPEN_WHITEBOARD": "打开白板", - "PEOPLE_VOTED": "人们投票", - "ADD_TO_CHAT": "加入聊天室", - "TAKE_PHOTO": "拍张照片", - "SET_THE_ANSWERS": "设定答案", - "ADD_ANOTHER_ANSWER": "添加另一个答案", - "ANSWER": "回答", - "IN_A_THREAD": "在话题中 ⤵", - "ERROR_GROUP_CREATE": "无法创建群组", - "TRY_AGAIN_LATER": "请稍后再试", - "CONTINUE": "继续", - "GROUP_NAME_MAX": "组名中最多允许 25 个字符", - "PASSWORD_MAX": "群组密码中最多允许 16 个字符", - "GROUP_PASSWORD": "群组密码", - "INCORRECT_PASSWORD": "密码不正确", - "OPEN_WHITEBOARD_TO_DRAW": "打开白板一起画画", - "CALL_HISTORY": "通话记录", - "NO_CALL_HISTORY": "还没有打过电话", - "CALL_DETAILS": "通话详情", - "CONFERENCE_CALL": "电话会议", - "NEW_GROUP": "新组", - "TRANSFER_OWNERSHIP": "转让所有权", - "COPY_MESSAGE": "复制", - "SHARE": "分享", - "OPEN_DOCUMENT_TO_DRAW": "打开文档一起绘制", - "INFORMATION": "信息", - "COPY_TEXT": "复制文本", - "FORWARD": "向前", - "TEXT": "文本", - "INCOMING_CALL": "来电", - "OUTGOING_CALL": "拨出电话", - "MISSED_CALL": "未接来电", - "NEW_CONVERSATION": "新聊天", - "FORWARDING": "正在发送消息...", - "READ": "阅读", - "No_RECIPIENT": "没有收件人", - "RECEIPT_INFORMATION": "收据信息", - "MESSAGE": "消息", - "GENERATING_SUMMARY":"正在生成摘要", - "CONVERSATION_SUMMARY":"对话摘要", - "GENERATE_SUMMARY":"生成摘要", - "COMETCHAT_BOT_FIRST_MESSAGE":"我怎样才能帮助你完成这次对话?请问我一个问题我会给你建议 🙂", - "COMETCHAT_ASK_BOT":"问", - "COMETCHAT_ASK_AI_BOT":"向 AI 机器人提问", - "COMETCHAT_ASK_BOT_SUBTITLE":"AI Bot", - "FORM_COMPLETION_MESSAGE": "感谢您填写表格。", - "FORM":"表格", - "CARD":"卡片", - "RECORDINGS":"录音", - "PARTICIPANTS":"参与者", - "NO_PARTICIPANTS":"没有参与者", - "NO_RECORDINGS":"没有录音", - "CALL_LOGS":"通话记录", - "CANCELLED_AUDIO_CALL":"语音通话已取消", - "CANCELLED_VIDEO_CALL":"视频通话取消", - "MICROPHONE_PERMISSION":"我们使用麦克风录制和共享音频信息。进入设置以启用麦克风访问权限" -} \ No newline at end of file + "USERS": "用户", + "CHATS": "聊天", + "GROUPS": "群组", + "MORE": "更多", + "MESSAGE_IMAGE": "📷 图片", + "MESSAGE_FILE": "📁 文件", + "MESSAGE_VIDEO": "📹 视频", + "MESSAGE_AUDIO": "🎵 音频", + "CUSTOM_MESSAGE": "你有一条消息", + "MISSED_VOICE_CALL": "未接语音通话", + "MISSED_VIDEO_CALL": "错过了视频通话", + "CUSTOM_MESSAGE_POLL": "📊 投票", + "CUSTOM_MESSAGE_STICKER": "💟 贴纸", + "CUSTOM_MESSAGE_DOCUMENT": "📃 文档", + "CUSTOM_MESSAGE_WHITEBOARD": "📝 白板", + "ONLINE": "在线", + "ADMINISTRATOR": "管理员", + "MODERATOR": "演示者", + "PARTICIPANT": "参与者", + "SUGGEST_A_REPLY": "建议回复", + "GENERATIONG_ICEBREAKER": "生成破冰船", + "PUBLIC": "公开", + "NO_REPLIES_FOUND": "未找到回复", + "PRIVATE": "私人", + "PASSWORD_PROTECTED": "密码保护", + "PRIVACY_AND_SECURITY": "隐私和安全", + "PREFERENCES": "首选项", + "MEMBERS": "会员", + "TODAY": "今天", + "YESTERDAY": "昨天", + "SUNDAY": "星期日", + "MONDAY": "星期一", + "TUESDAY": "星期二", + "WEDNESDAY": "周三", + "THURSDAY": "星期四", + "FRIDAY": "星期五", + "SATURDAY": "星期六", + "TYPING": "正在输入...", + "IS_TYPING": "正在打字...", + "CLOSE": "关闭", + "ENTER_GROUP_NAME": "输入群组名称", + "ADD_MEMBERS": "添加成员", + "SEND_MESSAGE": "发送消息", + "UNBLOCK_USER": "解除屏蔽用户", + "BLOCK_USER": "屏蔽用户", + "DELETE_AND_EXIT": "删除并退出", + "LEAVE_GROUP": "离开群组", + "CREATE_GROUP": "创建群组", + "SHARED_MEDIA": "共享媒体", + "VIDEO_CALL": "视频通话", + "AUDIO_CALL": "音频通话", + "LOADING": "加载中...", + "REPLY": "答复", + "REPLIES": "回复", + "LAUNCH": "启动", + "SHARED_COLLABORATIVE_DOCUMENT": "共享了一份协作文档", + "SHARED_COLLABORATIVE_WHITEBOARD": "共享了协作白板", + "CREATED_WHITEBOARD": "你已经创建了一个新的协作白板", + "CREATED_DOCUMENT": "你已经创建了一个新的 SOMCollaborative 文档", + "PHOTOS": "照片", + "VIDEOS": "视频", + "DOCUMENT": "文档", + "MESSAGE_IS_DELETED": "留言已删除", + "THIS_MESSAGE_DELETED": "⚠️ 此消息已删除", + "VIEW_ON_YOUTUBE": "在优酷上观看", + "SEARCH": "搜寻", + "NO_USERS_FOUND": "未找到用户", + "ERROR": "错误", + "NO_GROUPS_FOUND": "未找到群组", + "NO_CHATS_FOUND": "未找到聊天记录", + "MEDIA_MESSAGE": "媒体消息", + "INCOMING_AUDIO_CALL": "来电语音", + "INCOMING_VIDEO_CALL": "来电视频通话", + "DECLINE": "拒绝", + "ACCEPT": "接受", + "CALL_INITIATED": "呼叫已启动", + "OUTGOING_AUDIO_CALL": "拨出语音通话", + "OUTGOING_VIDEO_CALL": "拨出视频通话", + "CALL_REJECTED": "呼叫被拒绝", + "REJECTED_CALL": "呼叫被拒绝", + "CALL_ACCEPTED": "通话已接受", + "JOINED": "已加入", + "LEFT_THE_CALL": "离开了电话", + "UNANSWERED_AUDIO_CALL": "未接听的语音通话", + "UNANSWERED_VIDEO_CALL": "未接听的视频通话", + "CALL_ENDED": "通话已结束", + "CANCELLED_CALL": "已取消通话", + "CALL_BUSY": "通话忙碌", + "CALLING": "正在打电话...", + "ADD": "添加", + "NO_BANNED_MEMBERS_FOUND": "未找到被禁会员", + "BANNED_MEMBERS": "被禁止的会员", + "NAME": "姓名", + "SCOPE": "范围", + "UNBAN": "取消封禁", + "SELECT_GROUP_TYPE": "选择群组类型", + "ENTER_GROUP_PASSWORD": "输入群组密码", + "CREATE": "创建", + "CREATE_POLL": "创建投票", + "QUESTION": "问题", + "ENTER_YOUR_QUESTION": "输入你的问题", + "OPTIONS": "选项", + "ENTER_YOUR_OPTION": "输入您的选项", + "ADD_NEW_OPTION": "添加新选项", + "VIEW_MEMBERS": "查看会员", + "DETAILS": "详情", + "NOTIFICATIONS": "通知", + "OTHER": "其他", + "HELP": "帮帮我", + "REPORT_PROBLEM": "报告问题", + "GROUP_MEMBERS": "小组成员", + "BAN": "禁令", + "KICK": "踢", + "PICK_YOUR_EMOJI": "选择你的表情符号", + "PRIVATE_GROUP": "私人群组", + "PROTECTED_GROUP": "受保护群组", + "VISIT": "参观", + "ATTACH": "附上", + "ATTACH_FILE": "附上文件", + "ATTACH_VIDEO": "附上视频", + "ATTACH_AUDIO": "附加音频", + "ATTACH_IMAGE": "附上图片", + "COLLABORATIVE_DOCUMENT": "协作文档", + "COLLABORATIVE_WHITEBOARD": "协作白板", + "COLLABORATE_USING_DOCUMENT": "使用文档进行协作", + "COLLABORATE_USING_WHITEBOARD": "使用白板进行协作", + "EMOJI": "表情符号", + "ENTER_YOUR_MESSAGE_HERE": "在这里输入你的信息", + "NO_MESSAGES_FOUND": "这里还没有消息...", + "THREAD": "线程", + "ADD_REACTION": "添加反应", + "NO_STICKERS_FOUND": "未找到贴纸", + "REPLY_TO_THREAD": "回复话题", + "REPLY_IN_THREAD": "在话题中回复", + "DELETE_MESSAGE": "删除留言", + "EDIT_MESSAGE": "编辑消息", + "OWNER": "所有者", + "CHANGE_SCOPE": "变更范围", + "STICKER": "贴纸", + "LAST_ACTIVE_AT": "上次活跃时间", + "VOICE_CALL": "语音通话", + "VIEW_DETAIL": "查看详情", + "VOTES": "选票", + "VOTE": "投票", + "NO_VOTE": "不投票", + "REACTED": "反应", + "ADDED": "添加", + "UNBANNED": "未被禁止", + "MADE": "制作", + "UNANSWERED_CALL": "未接电话", + "MISSED_AUDIO_CALL": "错过的语音通话", + "ENTER_YOUR_PASSWORD": "输入你的密码", + "DOCS": "文档", + "NO_RECORDS_FOUND": "未找到任何记录", + "LIVE_REACTION": "现场反应", + "SMILEY_PEOPLE": "笑脸与人物", + "ANIMALES_NATURE": "动物与自然", + "FOOD_DRINK": "食物和饮料", + "ACTIVITY": "活动", + "TRAVEL_PLACES": "旅行与地点", + "OBJECTS": "物体", + "SYMBOLS": "符号", + "FLAGS": "旗帜", + "SENT": "已发送", + "SEEN": "看见了", + "DELIVERED": "已交付", + "TRANSLATE_MESSAGE": "翻译消息", + "LEFT": "左边", + "KICKED": "被踢的", + "BANNED": "禁止", + "NEW_MESSAGES": "新消息", + "NEW_MESSAGE": "新消息", + "JUMP": "跳", + "SELECT_VIDEO_SOURCE": "选择视频来源", + "SELECT_INPUT_AUDIO_SOURCE": "选择输入音频源", + "SELECT_OUTPUT_AUDIO_SOURCE": "选择输出音频源", + "INITIATED_GROUP_CALL": "已发起群组通话", + "YOU_INITIATED_GROUP_CALL": "你发起了群组通话", + "IGNORE": "忽略", + "ON_ANOTHER_CALL": "正在接另一个电话", + "CREATING": "正在创建", + "AVATAR": "阿凡达", + "ONGOING_CALL": "正在进行的通话", + "YOU_ALREADY_ONGOING_CALL": "您已经在通话中", + "RESIZE": "调整大小", + "SETTINGS": "设置", + "ACTIONS": "行动", + "VIEW_PROFILE": "查看个人资料", + "SEND_MESSAGE_IN_PRIVATE": "私下发送消息", + "DELETE": "删除", + "DELETE_CONFIRM": "你确定要删除吗?", + "CANCEL": "取消", + "LEAVE_CONFIRM": "你确定要离开群组吗?", + "TRANSFER_CONFIRM": "您是群组所有者;请在离开群组前将所有权转让给成员", + "ADDING": "正在添加...", + "TRANSFER": "转移", + "TRANSFERRING": "转移", + "YES": "是的", + "NO": "没有", + "SOMETHING_WRONG": "出了点问题;请再试一次。", + "INVALID_GROUP_NAME": "请输入群组的有效名称,然后重试", + "INVALID_PASSWORD": "请输入群组的有效密码并重试", + "INVALID_GROUP_TYPE": "请输入群组的有效类型,然后重试", + "WRONG_PASSWORD": "请输入正确的密码并重试", + "INVALID_POLL_QUESTION": "在创建投票之前,请输入必填的问题", + "INVALID_POLL_OPTION": "在创建投票之前,请输入所需的答案", + "SAME_LANGUAGE_MESSAGE": "选择的翻译语言与原始消息的语言类似", + "LEAVE": "离开", + "CUSTOM_MESSAGE_LOCATION": "📍 位置", + "IN_A_THREAD": "在话题中 ⤵", + "CALLS": "通话", + "OFFLINE": "离线", + "YOU": "你", + "PRIVACY": "隐私", + "BLOCKED_USERS": "被封锁的用户", + "YOU'VE_BLOCKED": "你已经封锁了", + "NO_PHOTOS": "没有照片", + "NO_VIDEOS": "没有视频", + "NO_DOCUMENTS": "没有文件", + "GENERATING_REPLIES": "生成回复", + "JOIN": "加入", + "DELETE_CONFIRM_MESSAGE": "你想删除这个对话吗?此对话将从您的所有设备中删除。", + "CHAT_ERROR_MESSAGE": "无法加载聊天记录。请再试一次", + "TRY_AGAIN": "再试一次", + "CONFIRM": "确认", + "UNSAFE_CONTENT": "不安全内容", + "UNSAFE_CONFIRMATION": "你一定想看看这些不安全的内容吗", + "AUDIO_FILE": "音频文件", + "SHARED_FILE": "共享文件", + "OPEN_DOCUMENT": "打开文档", + "OPEN_WHITEBOARD": "打开白板", + "PEOPLE_VOTED": "人们投了票", + "ADD_TO_CHAT": "加入聊天", + "TAKE_PHOTO": "拍张照片", + "SET_THE_ANSWERS": "设定答案", + "ADD_ANOTHER_ANSWER": "添加另一个答案", + "ANSWER": "回答", + "ERROR_GROUP_CREATE": "无法创建群组", + "TRY_AGAIN_LATER": "请稍后再试", + "CONTINUE": "继续", + "GROUP_NAME_MAX": "组名中最多允许 25 个字符", + "GROUP_PASSWORD_BLANK": "请输入密码", + "PASSWORD_MAX": "群组密码中最多允许 16 个字符", + "GROUP_PASSWORD": "群组密码", + "INCORRECT_PASSWORD": "密码不正确", + "OPEN_WHITEBOARD_TO_DRAW": "打开白板一起画画", + "CALL_HISTORY": "通话记录", + "NO_CALL_HISTORY": "还没有打过电话", + "CALL_DETAILS": "通话详情", + "CONFERENCE_CALL": "电话会议", + "NEW_GROUP": "新群组", + "TRANSFER_OWNERSHIP": "转让所有权", + "COPY_MESSAGE": "复制", + "SHARE": "分享", + "OPEN_DOCUMENT_TO_DRAW": "打开文档一起画画", + "INFORMATION": "留言信息", + "FORWARD": "向前", + "COPY_TEXT": "复制文本", + "TEXT": "文本", + "INCOMING_CALL": "来电", + "OUTGOING_CALL": "拨出电话", + "MISSED_CALL": "未接来电", + "GROUP_NAME_BLANK": "群组名称不应为空", + "GROUP_TYPE_BLANK": "群组类型不能为空", + "POLL_QUESTION_BLANK": "问题 canaut 为空", + "POLL_OPTION_BLANK": "“无法选项” 为空", + "NEW_CONVERSATION": "新聊天", + "FORWARDING": "正在发送消息...", + "READ": "阅读", + "No_RECIPIENT": "没有收件人", + "RECEIPT_INFORMATION": "收据信息", + "MESSAGE": "留言", + "GENERATING_SUMMARY": "生成摘要", + "CONVERSATION_SUMMARY": "对话摘要", + "GENERATE_SUMMARY": "生成摘要", + "COMETCHAT_BOT_FIRST_MESSAGE": "我怎样才能帮助你完成这次对话?请问我一个问题我会给你建议 🙂", + "COMETCHAT_ASK_BOT": "问", + "COMETCHAT_ASK_AI_BOT": "询问 AI 机器人", + "COMETCHAT_ASK_BOT_SUBTITLE": "AI 机器人", + "FORM_COMPLETION_MESSAGE": "感谢您填写表格。", + "FORM": "表格", + "CARD": "卡片", + "RECORDINGS": "录音", + "PARTICIPANTS": "参与者", + "NO_PARTICIPANTS": "没有参与者", + "NO_RECORDINGS": "没有录音", + "CALL_LOGS": "通话记录", + "CANCELLED_AUDIO_CALL": "取消了语音通话", + "CANCELLED_VIDEO_CALL": "视频通话已取消", + "MEET_WITH": "与... 会面", + "MIN_MEETING": "我的会议", + "MORE_TIMES": "更多次数", + "SELECT_DAY": "选择一天", + "SELECT_TIME": "选择时间", + "NO_TIME_SLOT_AVAILABLE": "此日期没有可用的时段。请尝试其他日期。", + "BOOK_NEW_SLOT": "预订新老虎机", + "TRY_AGAIN_CAMEL": "再试一次", + "TIME_SLOT_UNAVAILABLE": "时段不再可用。请选择另一个。", + "MEETING_SCHEDULER": "会议安排器", + "MIN": "mi" +} diff --git a/src/shared/resources/CometChatTheme/Palette.ts b/src/shared/resources/CometChatTheme/Palette.ts index b23cf05..85734a5 100644 --- a/src/shared/resources/CometChatTheme/Palette.ts +++ b/src/shared/resources/CometChatTheme/Palette.ts @@ -119,6 +119,8 @@ class Palette { mode: string backgroundColor: PaletteItem primary: PaletteItem + primary1: PaletteItem + primary13: PaletteItem primary40: PaletteItem secondary: PaletteItem error: PaletteItem @@ -144,6 +146,14 @@ class Palette { [modes.light]: 'rgb(51, 153, 255)', [modes.dark]: 'rgb(51, 153, 255)', }), + primary1 = new PaletteItem({ + [modes.light]: 'rgba(51, 153, 255, 1)', + [modes.dark]: 'rgba(51, 153, 255, 1)', + }), + primary13 = new PaletteItem({ + [modes.light]: 'rgba(51, 153, 255, .13)', + [modes.dark]: 'rgba(51, 153, 255, .13)', + }), primary40 = new PaletteItem({ [modes.light]: 'rgba(51, 153, 255, .04)', [modes.dark]: 'rgba(51, 153, 255, .04)', @@ -208,6 +218,8 @@ class Palette { this.mode = mode; this.backgroundColor = backgroundColor; this.primary = primary; + this.primary1 = primary1; + this.primary13 = primary13; this.primary40 = primary40; this.secondary = secondary; this.error = error; @@ -270,6 +282,12 @@ class Palette { getPrimary = () => { return this.primary[this.mode]; }; + getPrimary1 = () => { + return this.primary1[this.mode]; + }; + getPrimary13 = () => { + return this.primary13[this.mode]; + }; getPrimary40 = () => { return this.primary40[this.mode]; } @@ -292,11 +310,23 @@ class Palette { } setPrimary(colorset) { + if (colorset && colorset[modes.light] && colorset[modes.dark]) { + this.primary1 = colorset; + } + } + + setPrimary1(colorset) { if (colorset && colorset[modes.light] && colorset[modes.dark]) { this.primary = colorset; } } + setPrimary13(colorset) { + if (colorset && colorset[modes.light] && colorset[modes.dark]) { + this.primary13 = colorset; + } + } + setPrimary40(colorset) { if (colorset && colorset[modes.light] && colorset[modes.dark]) { this.primary40 = colorset; diff --git a/src/shared/resources/CometChatTheme/Typography.ts b/src/shared/resources/CometChatTheme/Typography.ts index 0257f6b..3b08d24 100644 --- a/src/shared/resources/CometChatTheme/Typography.ts +++ b/src/shared/resources/CometChatTheme/Typography.ts @@ -35,11 +35,17 @@ class Typography { fontWeightSemibold: "600" fontWeightBold: "700" heading: FontStyle + heading2: FontStyle + heading3: FontStyle name: FontStyle title1: FontStyle title2: FontStyle subtitle1: FontStyle subtitle2: FontStyle + subtitle3: FontStyle + subtitle4: FontStyle + subtitle5: FontStyle + subtitle6: FontStyle text1: FontStyle text2: FontStyle caption1: FontStyle @@ -58,6 +64,18 @@ class Typography { fontSize: 22, }), + heading2 = new FontStyle({ + fontFamily: fontFamily, + fontWeight: fontWeightBold, + fontSize: 20, + }), + + heading3 = new FontStyle({ + fontFamily: fontFamily, + fontWeight: fontWeightBold, + fontSize: 18, + }), + name = new FontStyle({ fontFamily: fontFamily, fontWeight: fontWeightMedium, @@ -88,6 +106,30 @@ class Typography { fontSize: 13, }), + subtitle3 = new FontStyle({ + fontFamily: fontFamily, + fontWeight: fontWeightRegular, + fontSize: 12, + }), + + subtitle4 = new FontStyle({ + fontFamily: fontFamily, + fontWeight: fontWeightRegular, + fontSize: 11, + }), + + subtitle5 = new FontStyle({ + fontFamily: fontFamily, + fontWeight: fontWeightRegular, + fontSize: 10, + }), + + subtitle6 = new FontStyle({ + fontFamily: fontFamily, + fontWeight: fontWeightRegular, + fontSize: 9, + }), + text1 = new FontStyle({ fontFamily: fontFamily, fontWeight: fontWeightMedium, @@ -123,11 +165,18 @@ class Typography { this.fontWeightSemibold = fontWeightSemibold; this.fontWeightBold = fontWeightBold; this.heading = new FontStyle(heading); + this.heading2 = new FontStyle(heading2); + this.heading3 = new FontStyle(heading3); this.name = new FontStyle(name); this.title1 = new FontStyle(title1); this.title2 = new FontStyle(title2); this.subtitle1 = new FontStyle(subtitle1); this.subtitle2 = new FontStyle(subtitle2); + this.subtitle2 = new FontStyle(subtitle2); + this.subtitle3 = new FontStyle(subtitle3); + this.subtitle4 = new FontStyle(subtitle4); + this.subtitle5 = new FontStyle(subtitle5); + this.subtitle6 = new FontStyle(subtitle6); this.text1 = new FontStyle(text1); this.text2 = new FontStyle(text2); this.caption1 = new FontStyle(caption1); @@ -165,6 +214,26 @@ class Typography { } } + setHeading2(headingFont) { + if (headingFont && headingFont.fontSize) { + this.heading2.fontSize = headingFont.fontSize; + } + + if (headingFont && headingFont.fontWeight) { + this.heading2.fontWeight = headingFont.fontWeight; + } + } + + setHeading3(headingFont) { + if (headingFont && headingFont.fontSize) { + this.heading3.fontSize = headingFont.fontSize; + } + + if (headingFont && headingFont.fontWeight) { + this.heading3.fontWeight = headingFont.fontWeight; + } + } + setName(nameFont) { if (nameFont && nameFont.fontSize) { this.name.fontSize = nameFont.fontSize; @@ -215,6 +284,46 @@ class Typography { } } + setSubtitle3(subtitleFont) { + if (subtitleFont && subtitleFont.fontSize) { + this.subtitle3.fontSize = subtitleFont.fontSize; + } + + if (subtitleFont && subtitleFont.fontWeight) { + this.subtitle3.fontWeight = subtitleFont.fontWeight; + } + } + + setSubtitle4(subtitleFont) { + if (subtitleFont && subtitleFont.fontSize) { + this.subtitle4.fontSize = subtitleFont.fontSize; + } + + if (subtitleFont && subtitleFont.fontWeight) { + this.subtitle4.fontWeight = subtitleFont.fontWeight; + } + } + + setSubtitle5(subtitleFont) { + if (subtitleFont && subtitleFont.fontSize) { + this.subtitle5.fontSize = subtitleFont.fontSize; + } + + if (subtitleFont && subtitleFont.fontWeight) { + this.subtitle5.fontWeight = subtitleFont.fontWeight; + } + } + + setSubtitle6(subtitleFont) { + if (subtitleFont && subtitleFont.fontSize) { + this.subtitle6.fontSize = subtitleFont.fontSize; + } + + if (subtitleFont && subtitleFont.fontWeight) { + this.subtitle6.fontWeight = subtitleFont.fontWeight; + } + } + setText1(textFont) { if (textFont && textFont.fontSize) { this.text1.fontSize = textFont.fontSize; diff --git a/src/shared/utils/InteractiveMessageUtils.ts b/src/shared/utils/InteractiveMessageUtils.ts index fdda6e2..cb17557 100644 --- a/src/shared/utils/InteractiveMessageUtils.ts +++ b/src/shared/utils/InteractiveMessageUtils.ts @@ -1,6 +1,5 @@ import { MessageTypeConstants } from "../constants/UIKitConstants"; -import { CardMessage, CustomInteractiveMessage, FormMessage } from "../modals/InteractiveData"; - +import { CardMessage, CustomInteractiveMessage, FormMessage, SchedulerMessage } from "../modals/InteractiveData"; export class InteractiveMessageUtils { static convertInteractiveMessage(item: any): any { @@ -10,6 +9,8 @@ export class InteractiveMessageUtils { switch (item.getType()) { case MessageTypeConstants.form: return FormMessage.fromJSON(item); + case MessageTypeConstants.scheduler: + return SchedulerMessage.fromJSON(item); case MessageTypeConstants.card: return CardMessage.fromJSON(item); default: diff --git a/src/shared/utils/SchedulerUtils.ts b/src/shared/utils/SchedulerUtils.ts new file mode 100644 index 0000000..d4cfbe8 --- /dev/null +++ b/src/shared/utils/SchedulerUtils.ts @@ -0,0 +1,196 @@ +import { DateTime } from "../libs/luxon/src/luxon"; +export const convert24to12 = (time) => { + if (time && typeof time === "string") { + let [hours, minutes] = time.split(":"); // split hours and minutes + let suffix = +hours >= 12 ? "PM" : "AM"; // set suffix as AM or PM + hours = (+hours % 12 || 12).toString(); // convert hours from 24 to 12 hour format + return `${hours}:${minutes} ${suffix}`; // return the format as a string + } + return ""; +}; + +export const addMinutes = (time: string, minutes: number) => { + // create a new date object + const date = new Date(); + + // split the time string into hours and minutes + const parts = time.split(":"); + + // set the time on the date object + date.setHours(+parts[0]); + date.setMinutes(+parts[1]); + + // add the minutes + date.setMinutes(date.getMinutes() + minutes); + + // format and return the new time + return `${date + .getHours() + .toString() + .padStart(2, "0")}${date.getMinutes().toString().padStart(2, "0")}`; +}; + +export const convertToATimeZone = ( + time: number | string, + timeZone: string, + formats: "yyyy-MM-dd" | "HHmm" | string, + usingFunc: "fromMillis" | "fromFormat" | "fromISO", + fromFormat?: string +) => { + if (usingFunc === "fromMillis" && typeof time === "number") { + //@ts-ignore + let timeZoneTime = DateTime.fromMillis(time, { zone: timeZone }); + let localTime = timeZoneTime.setZone(); + return localTime.toFormat(formats); + } + if (usingFunc === "fromFormat" && typeof time === "string") { + //@ts-ignore + let timeZoneTime = DateTime.fromFormat(time, fromFormat, { + zone: timeZone, + }); + let localTime = timeZoneTime.setZone(); + return localTime.toFormat(formats); + } + if (usingFunc === "fromISO" && typeof time === "string") { + //@ts-ignore + let timeZoneTime = DateTime.fromISO(time, { + zone: timeZone, + }); + let localTime = timeZoneTime.setZone(); + if (formats === "toISO") { + return localTime; + } + return localTime.toFormat(formats); + } + return ""; +}; + +export const convertToLocalTimeZone = ( + time: string, + timeZone: string, + formats: "yyyy-MM-dd" | "HHmm" | string +) => { + const isoTime = DateTime.fromISO(time, { + zone: timeZone, + }); + const utcTime = isoTime.toUTC(); + const timeZoneOffsetInMins = new Date().getTimezoneOffset(); + const localTime = utcTime + .minus({ minutes: timeZoneOffsetInMins }) + .toFormat(formats); + return localTime; +}; + +export const convertDate = (dateStr) => { + const year = dateStr.slice(0, 4); + const month = dateStr.slice(4, 6); + const day = dateStr.slice(6, 8); + const time = dateStr.slice(9); + return `${year}-${month}-${day}T${time.slice(0, 2)}:${time.slice( + 2, + 4 + )}:${time.slice(4)}`; +}; + +export const getFormatedDateString = ({ + date, + format, +}: { + date: string | Date | number; + format: "YYYY-DD-MM" | "HHmm" | string; +}) => { + if (typeof date === "number") { + return DateTime.fromMillis(date).toFormat(format); + } + if (typeof date === "object") { + return DateTime.fromISO(date.toISOString()).toFormat(format); + } + return ""; +}; +export const getMinSlotsFromRange = ({ + startTime, + endTime, + slotDuration = 30, + selectedSlots = [], + bufferDuration = 0, +}) => { + const slots = []; // Array to hold all available slots + + // Convert start time and end time (HHMM format) to total minutes + let currentStartMinutes = + Number(startTime.slice(0, 2)) * 60 + Number(startTime.slice(2)); + let endMinutesTotal = + Number(endTime.slice(0, 2)) * 60 + Number(endTime.slice(2)); + if (endMinutesTotal === 1439) endMinutesTotal += 1; + + // Add buffer time if the availability starts immediately after a blocked time + const prevSlot = selectedSlots.find( + (slot) => + Number(slot.endTime!.slice(0, 2)) * 60 + + Number(slot.endTime!.slice(2)) === + currentStartMinutes + ); + if (prevSlot) currentStartMinutes += bufferDuration; + + // Keep generating slots until the end of the availability range + while (currentStartMinutes + slotDuration <= endMinutesTotal) { + let conflictSlot = null; + + // Iterate over each selected slot to find a conflict + for (let i = 0; i < selectedSlots.length; i++) { + const { startTime: start, endTime: end } = selectedSlots[i]; + const slotStart = Number(start.slice(0, 2)) * 60 + Number(start.slice(2)); + const slotEnd = Number(end.slice(0, 2)) * 60 + Number(end.slice(2)); + + let currentEndMinutes = currentStartMinutes + slotDuration; + + // The current slot is within the selected slot or + // The current slot starts before and ends after the start of the selected slot or + // The current slot starts before the end of the selected slot but ends after + if ( + (currentStartMinutes >= slotStart && currentEndMinutes <= slotEnd) || + (currentStartMinutes < slotStart && currentEndMinutes > slotStart) || + (currentStartMinutes < slotEnd && currentEndMinutes > slotEnd) + ) { + conflictSlot = selectedSlots[i]; + break; + } + } + + // If there is no conflict, add the slot + if (!conflictSlot) { + slots.push( + `${String(Math.floor(currentStartMinutes / 60)).padStart( + 2, + "0" + )}:${String(currentStartMinutes % 60).padStart(2, "0")}` + ); + } else { + // If a conflict arises, remove the last added slot if it is not possible to add the buffer duration to it without overlapping the selected slot + if ( + slots.length && + Number(conflictSlot.startTime.slice(0, 2)) * 60 + + Number(conflictSlot.startTime.slice(2)) < + Number(slots[slots.length - 1].slice(0, 2)) * 60 + + Number(slots[slots.length - 1].slice(3)) + + slotDuration + + bufferDuration + ) { + slots.pop(); + } + + // Skip to the end of the conflict slot and continue to the next iteration + currentStartMinutes = + Number(conflictSlot.endTime.slice(0, 2)) * 60 + + Number(conflictSlot.endTime.slice(2)) + + bufferDuration; + continue; + } + + // Move to the next slot by adding the slot duration + currentStartMinutes += slotDuration; + } + + // Return all available slots + return slots; +}; diff --git a/src/shared/utils/conversationUtils.ts b/src/shared/utils/conversationUtils.ts index 8aba6e8..c265c2c 100644 --- a/src/shared/utils/conversationUtils.ts +++ b/src/shared/utils/conversationUtils.ts @@ -1,6 +1,7 @@ import { localize } from "../resources/CometChatLocalize"; import { CometChatOptions } from "../modals/CometChatOptions"; import { CometChat } from "@cometchat/chat-sdk-react-native"; +import { MessageTypeConstants } from "../constants/UIKitConstants"; export class CometChatConversationUtils { @@ -71,7 +72,13 @@ export class CometChatConversationUtils { } msgText = (lastMessage as CometChat.Action).getMessage(); } else if (lastMessage.getCategory() === CometChat.CATEGORY_INTERACTIVE) { - msgText = lastMessage.getType() === "form" ? `${localize('FORM')} 📋` : `${localize('CARD')} 🪧`; + msgText = lastMessage.getType() === "form" + ? `${localize('FORM')} 📋` + : lastMessage.getType() === MessageTypeConstants.scheduler + ? (lastMessage?.interactiveData?.title ? + `🗓️ ${lastMessage?.interactiveData?.title}` + : `🗓️ ${localize('MEET_WITH')} ${lastMessage?.getSender()?.getName()}`) + : `${localize('CARD')} 🪧`; } else { msgText = lastMessage['metaData']?.pushNotification; diff --git a/src/shared/utils/icsToJson.js b/src/shared/utils/icsToJson.js new file mode 100644 index 0000000..b6d6613 --- /dev/null +++ b/src/shared/utils/icsToJson.js @@ -0,0 +1,113 @@ +const NEW_LINE = /\r\n|\n|\r/; + +const EVENT = 'VEVENT'; +const EVENT_START = 'BEGIN'; +const EVENT_END = 'END'; +const START_DATE = 'DTSTART'; +const END_DATE = 'DTEND'; +const DESCRIPTION = 'DESCRIPTION'; +const SUMMARY = 'SUMMARY'; +const LOCATION = 'LOCATION'; +const ALARM = 'VALARM'; +const TZID = 'TZID'; +const TZOFFSETFROM = 'TZOFFSETFROM'; +const TZOFFSETTO = 'TZOFFSETTO'; + +const keyMap = { + [START_DATE]: 'startDate', + [END_DATE]: 'endDate', + [DESCRIPTION]: 'description', + [SUMMARY]: 'summary', + [LOCATION]: 'location', + [TZID]: 'tzid', + [TZOFFSETFROM]: 'tzOffsetFrom', + [TZOFFSETTO]: 'tzOffsetTo', +}; + +const clean = (string) => unescape(string).trim(); + +const icsToJson = (icsData) => { + const array = []; + let currentObj = {}; + let timeZoneObj = {}; + let lastKey = ''; + + const lines = icsData.split(NEW_LINE); + + let isAlarm = false; + for (let i = 0, iLen = lines.length; i < iLen; ++i) { + const line = lines[i]; + const lineData = line.split(':'); + + let key = lineData[0]; + const value = lineData[1]; + let kyParts = []; + // console.log('--->>> ',{key:key,value:value}); + if (key.indexOf(';') !== -1) { + const keyParts = key.split(';'); + key = keyParts[0]; + kyParts = keyParts; + // Maybe do something with that second part later + } + + if (lineData.length < 2) { + if (key.startsWith(' ') && lastKey !== undefined && lastKey.length) { + currentObj[lastKey] += clean(line.substr(1)); + } + continue; + } else { + lastKey = keyMap[key]; + } + switch (key) { + case EVENT_START: + if (value === EVENT) { + currentObj = { ...timeZoneObj }; + } else if (value === ALARM) { + isAlarm = true; + } + break; + case EVENT_END: + isAlarm = false; + if (value === EVENT) { + timeZoneObj = {}; + array.push(currentObj); + } + break; + case START_DATE: + // console.log({ key, value, lineData, kyParts }); + if (kyParts.length > 1) { + let tzid = kyParts[1]?.split('='); + if (tzid.length > 1) currentObj[keyMap[TZID]] = tzid[1]; + } + currentObj[keyMap[START_DATE]] = value; + break; + case END_DATE: + currentObj[keyMap[END_DATE]] = value; + break; + case DESCRIPTION: + if (!isAlarm) currentObj[keyMap[DESCRIPTION]] = clean(value); + break; + case SUMMARY: + currentObj[keyMap[SUMMARY]] = clean(value); + break; + case LOCATION: + currentObj[keyMap[LOCATION]] = clean(value); + break; + case TZID: + timeZoneObj[keyMap[TZID]] = value; + break; + case TZOFFSETFROM: + timeZoneObj[keyMap[TZOFFSETFROM]] = value; + break; + case TZOFFSETTO: + timeZoneObj[keyMap[TZOFFSETTO]] = value; + break; + default: + continue; + } + } + + return array; +}; + +export default icsToJson; diff --git a/src/shared/views/CometChatCheckBox/CometChatCheckBox.tsx b/src/shared/views/CometChatCheckBox/CometChatCheckBox.tsx index 742bd3c..abc261c 100644 --- a/src/shared/views/CometChatCheckBox/CometChatCheckBox.tsx +++ b/src/shared/views/CometChatCheckBox/CometChatCheckBox.tsx @@ -45,12 +45,12 @@ const CometChatCheckBox = (props: CometChatCheckBoxInterface) => { return selectedOptions.find((selectedOption) => selectedOption === option.getValue()) ? true : false; } return ( - <View style={{ marginVertical: 10 }}> + <View style={{ marginBottom: 12 }}> - <Text style={[titleFont, { color: titleColor }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> + <Text style={[titleFont, { color: titleColor, marginBottom: 4 }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> {data.getOptions().map((option, index) => ( - <View key={index} style={{ flexDirection: "row", alignItems: "center", marginTop: 3 }}> + <View key={index} style={{ flexDirection: "row", alignItems: "center", marginVertical: 1 }}> <TouchableOpacity style={{ height: 20, width: 20, backgroundColor: _checkSelectedOption(option) ? activeBackgroundColor : inactiveBackgroundColor, borderRadius: 5, marginRight: 5, alignItems: "center", justifyContent: "center", diff --git a/src/shared/views/CometChatDateTimePicker/CometChatDateTimePicker.tsx b/src/shared/views/CometChatDateTimePicker/CometChatDateTimePicker.tsx new file mode 100644 index 0000000..a9d8693 --- /dev/null +++ b/src/shared/views/CometChatDateTimePicker/CometChatDateTimePicker.tsx @@ -0,0 +1,189 @@ +import { + StyleSheet, + Text, + View, + Image, + TouchableOpacity, + Platform, +} from "react-native"; +import React, { useState, useContext, useLayoutEffect } from "react"; +import DateTimePickerModal from "../../libs/datePickerModal"; +import { ICONS } from "../../assets/images"; +import { CometChatContextType } from "../../base"; +import { CometChatContext } from "../../CometChatContext"; +import { DateTimeElement } from "../../modals/InteractiveData/InteractiveElements/DateTimeElement"; +import { DateTime } from "../../libs/luxon/src/luxon"; +import { convertToATimeZone } from "../../utils/SchedulerUtils"; +import DateTimePicker from "@react-native-community/datetimepicker"; +import { styles } from "./DateTimePickerStyle"; + +export interface CometChatDateTimePickerInterface { + style: object; + data: DateTimeElement; + showError: boolean; + onChange: (selectedDateTime: string) => void; +} +export const CometChatDateTimePicker = ( + props: CometChatDateTimePickerInterface +) => { + const { style, showError, data, onChange } = props; + const mode = data.getMode(); + const format = data.getDateTimeFormat(); + const timeZone = data.getTimeZone(); + + const { theme } = useContext<CometChatContextType>(CometChatContext); + + const [isDatePickerVisible, setDatePickerVisibility] = useState(false); + + const [defaultTime, setDefaultTime] = useState(data.getDefaultValue() ?? ""); + const [selectedDate, setSelectedDate] = useState(); + const [fromDateTime, setFromDateTime] = useState( + data.getFromDateTime() ?? "" + ); + const [toDateTime, setToDateTime] = useState(data.getToDateTime() ?? ""); + + const onDateSelected = (e) => { + setSelectedDate(() => { + if (mode === "time" && Platform.OS === "ios") { + return selectedDate; + } else return e; + }); + e = DateTime.fromJSDate(e).setZone(); + setDatePickerVisibility(false); + if (mode === "date") { + e = DateTime.fromISO(e).toFormat("yyyy-MM-dd"); + } + if (mode === "time") { + if (Platform.OS === "ios") { + e = DateTime.fromJSDate(selectedDate).toFormat("HH:mm"); + } else e = DateTime.fromISO(e).toFormat("HH:mm"); + } + + onChange && onChange(e); + }; + + const formatDate = (time, fromJSDate = false) => { + if (time && format) { + if (fromJSDate) + return DateTime.fromJSDate(new Date(time)).toFormat(format); + return DateTime.fromISO(time).toFormat(format); + } + return ""; + }; + + const convertToATimeZoneWithModes = (time) => { + switch (mode) { + case "dateTime": + return convertToATimeZone(time, timeZone, "toISO", "fromISO"); + case "date": + return convertToATimeZone( + `${time}T00:00:00`, + timeZone, + "toISO", + "fromISO" + ); + case "time": + time = time?.split(":"); + if(time && time.length) { + let dateTime = new Date(); + dateTime.setHours(time[0]); + dateTime.setMinutes(time[1]); + if (time.length === 3) { + dateTime.setSeconds(time[2]); + } else { + dateTime.setSeconds(0); + } + return convertToATimeZone( + dateTime.toISOString(), + timeZone, + "toISO", + "fromISO" + ); + } + return ""; + default: + return ""; + } + }; + useLayoutEffect(() => { + let defaultTime = convertToATimeZoneWithModes(data.getDefaultValue()); + let fromDateTime = convertToATimeZoneWithModes(data.getFromDateTime()); + let toDateTime = convertToATimeZoneWithModes(data.getToDateTime()); + setDefaultTime(defaultTime); + setSelectedDate(defaultTime); + setFromDateTime(fromDateTime); + setToDateTime(toDateTime); + }, []); + + return ( + <View style={{ marginBottom: 12 }}> + {Boolean(defaultTime) && Boolean(fromDateTime) && Boolean(toDateTime) && ( + <DateTimePickerModal + isVisible={isDatePickerVisible} + date={ + selectedDate instanceof Date ? selectedDate : new Date(selectedDate) + } + mode={mode?.toLowerCase() || "datetime"} + onConfirm={onDateSelected} + onCancel={() => setDatePickerVisibility(false)} + minimumDate={new Date(fromDateTime)} + maximumDate={new Date(toDateTime)} + display={Platform.OS === "ios" ? "inline" : "default"} + {...(Platform.OS === "ios" && mode?.toLowerCase() === "time" + ? { + customPickerIOS: () => ( + <DateTimePicker + value={ + selectedDate instanceof Date + ? selectedDate + : new Date(selectedDate) + } + mode="time" + is24Hour={true} + display="spinner" + onChange={(event, date) => { + setSelectedDate(date); + }} + /> + ), + } + : {})} + /> + )} + <Text + style={[ + theme.typography.subtitle1, + { color: theme.palette.getAccent800() ?? style.titleColor, marginBottom: 4 }, + style.titleFont || {}, + ]} + > + {data.getLabel()} + {!data.getOptional() && "*"} + </Text> + + <TouchableOpacity + style={[ + styles.dateContainerView, + { + borderColor: showError + ? theme.palette.getError() + : theme.palette.getAccent200(), + }, + style.border || {}, + ]} + onPress={() => { + setDatePickerVisibility(true); + }} + > + <Text> + {formatDate(selectedDate || defaultTime, Boolean(selectedDate))} + </Text> + <Image style={{ height: 20, width: 20 }} source={ICONS.CALENDAR} /> + </TouchableOpacity> + </View> + ); +}; + +CometChatDateTimePicker.defaultProps = { + style: {}, +}; diff --git a/src/shared/views/CometChatDateTimePicker/DateTimePickerStyle.ts b/src/shared/views/CometChatDateTimePicker/DateTimePickerStyle.ts new file mode 100644 index 0000000..612d475 --- /dev/null +++ b/src/shared/views/CometChatDateTimePicker/DateTimePickerStyle.ts @@ -0,0 +1,19 @@ +import { StyleSheet } from "react-native"; +import { BorderStyleInterface, FontStyle } from "../../base"; + +export interface DatePickerStyleInterface { + titleFont?: FontStyle; + titleColor?: string; + border?: BorderStyleInterface; +} +export const styles = StyleSheet.create({ + dateContainerView: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 10, + height: 38, + borderWidth: 1, + borderRadius: 5, + }, +}); diff --git a/src/shared/views/CometChatDateTimePicker/index.ts b/src/shared/views/CometChatDateTimePicker/index.ts new file mode 100644 index 0000000..d903431 --- /dev/null +++ b/src/shared/views/CometChatDateTimePicker/index.ts @@ -0,0 +1,5 @@ +export { + CometChatDateTimePickerInterface, + CometChatDateTimePicker, +} from "./CometChatDateTimePicker"; +export { DatePickerStyleInterface } from "./DateTimePickerStyle"; diff --git a/src/shared/views/CometChatDropDown/CometChatDropDown.tsx b/src/shared/views/CometChatDropDown/CometChatDropDown.tsx index d492a2f..a762da1 100644 --- a/src/shared/views/CometChatDropDown/CometChatDropDown.tsx +++ b/src/shared/views/CometChatDropDown/CometChatDropDown.tsx @@ -65,7 +65,7 @@ const CometChatDropdown = (props: CometChatButtonInterface) => { return ( <View style={{ ...styles.defaultDropdownStyle, ...dropdownStyle }}> - <Text style={[titleFont, { color: titleColor }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> + <Text style={[titleFont, { color: titleColor, marginBottom: 4 }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> <View style={{ position: "relative" }}> <TouchableOpacity onPress={onDropDownToggle} style={[styles.dropdownButton, { borderColor: showError ? theme.palette.getError() : border.borderColor, borderWidth: border.borderWidth }]}> @@ -100,13 +100,13 @@ const styles = StyleSheet.create({ defaultDropdownStyle: { // position: "relative", zIndex: 1, - marginVertical: 10, + marginBottom: 12, // Your default dropdown style here }, dropdownButton: { borderWidth: 1, flexDirection: "row", justifyContent: "space-between", height: 35, - alignItems: "center", paddingHorizontal: 8, marginTop: 5, + alignItems: "center", paddingHorizontal: 8, // Your dropdown button style here }, dropdownArrow: { diff --git a/src/shared/views/CometChatFormBubble/CometChatFormBubble.tsx b/src/shared/views/CometChatFormBubble/CometChatFormBubble.tsx index 64b45bd..1493fb9 100644 --- a/src/shared/views/CometChatFormBubble/CometChatFormBubble.tsx +++ b/src/shared/views/CometChatFormBubble/CometChatFormBubble.tsx @@ -1,5 +1,9 @@ -import React, { useContext, useEffect, useRef, useCallback } from "react"; -import { View, Text, NativeModules } from "react-native"; +import React, { useContext, useEffect, useState, useRef, useCallback } from "react"; +import { + View, + Text, + NativeModules, +} from "react-native"; import { CometChatContextType } from "../../base/Types"; import { CometChatContext } from "../../CometChatContext"; @@ -20,8 +24,14 @@ import { localize } from "../../resources"; import { memo } from "react"; import { ButtonStyle, CometChatButton } from "../CometChatButton"; import CometChatLabel from "../CometChatLabel/CometChatLabel"; +import { DateTimeElement } from "../../modals/InteractiveData/InteractiveElements/DateTimeElement"; +import { CometChatDateTimePicker } from "../CometChatDateTimePicker"; +import { CometChatUIKit } from "../../CometChatUiKit"; +import { CometChatUiKitConstants } from "../.."; const WebView = NativeModules['WebViewManager']; +const { TimeZoneCodeManager } = NativeModules; + export interface CometChatFormBubbleInterface { message: FormMessage, // Expect JSON object style?: FormBubbleStyleInterface, @@ -47,10 +57,11 @@ export const CometChatFormBubble = memo((props: CometChatFormBubbleInterface) => const [interactionGoal, setInteractionGoal] = React.useState<CometChat.InteractionGoal>(undefined); const [loaderButton, setLoaderButton] = React.useState<string>(""); const [loggedInUser, setLoggedInUser] = React.useState<CometChat.User>(null); + const currentTimeZoneRef = useRef(""); const _style = new FormBubbleStyle({ wrapperPadding: 3, - titleFont: theme.typography.heading, + titleFont: {...theme.typography.heading2, fontWeight: '400'}, titleColor: theme.palette.getAccent(), goalCompletionTextColor: theme.palette.getAccent600(), backgroundColor: theme.palette.getBackgroundColor(), @@ -87,6 +98,7 @@ export const CometChatFormBubble = memo((props: CometChatFormBubbleInterface) => radioButtonStyle, singleSelectStyle, textInputStyle, + datePickerStyle // Extract style properties } = _style; @@ -99,6 +111,9 @@ export const CometChatFormBubble = memo((props: CometChatFormBubbleInterface) => console.log("Error while getting loggedInUser"); setLoggedInUser(null); }); + TimeZoneCodeManager.getCurrentTimeZone((timeZone) => { + currentTimeZoneRef.current = timeZone; + }); setInteractedElements(message.getInteractions() || []); setInteractionGoal(message.getInteractionGoal() || undefined); let formFields = {}; @@ -240,7 +255,7 @@ export const CometChatFormBubble = memo((props: CometChatFormBubbleInterface) => } return ( - <View style={{ opacity: isDisabled() ? .7 : 1, marginVertical: 5 }}> + <View style={{ opacity: isDisabled() ? .7 : 1, marginVertical: 8 }}> <CometChatButton onPress={isDisabled() ? () => { } : onClick} text={data.getButtonText()} @@ -261,6 +276,25 @@ export const CometChatFormBubble = memo((props: CometChatFormBubbleInterface) => ); } + const _renderDateTimePicker = (data: DateTimeElement) => { + !data.getOptional() && allRequiredFields.push(data.getElementId()); + + function onChange(time: string) { + setBodyData({ ...bodyData, [data.getElementId()]: time }); + if (time) { + removeFromRequiredList(data.getElementId()); + } + } + return ( + <CometChatDateTimePicker + data={data} + style={datePickerStyle} + showError={currentRequiredFields.includes(data.getElementId())} + onChange={onChange} + /> + ); + }; + const renderField = (field: ElementEntity) => { switch (field.getElementType()) { @@ -285,6 +319,9 @@ export const CometChatFormBubble = memo((props: CometChatFormBubbleInterface) => case ElementType.button: // Render button return _renderButton(field as ButtonElement); + case ElementType.dateTime: + // Render dateTimepicker + return _renderDateTimePicker(field as DateTimeElement); // Handle other cases default: @@ -310,7 +347,18 @@ export const CometChatFormBubble = memo((props: CometChatFormBubbleInterface) => } } - function _handleButtonClick(action: ButtonElement["action"], elementId: string, isSubmitElement?: boolean) { + async function _handleButtonClick(action: ButtonElement["action"], elementId: string, isSubmitElement?: boolean) { + let conversation = await CometChat.CometChatHelper.getConversationFromMessage( + message + ).then( + (conversation) => { + return conversation; + }, + (error) => { + console.log("Error while converting message object", error); + return undefined; + } + ); if (isSubmitElement && onSubmitClick) { const dataKey = (action as APIAction)?.getDataKey() || "CometChatData" let payload: any = (action as APIAction).getPayload() || {} @@ -324,17 +372,36 @@ export const CometChatFormBubble = memo((props: CometChatFormBubbleInterface) => switch (action.getActionType()) { case ButtonAction.apiAction: if (filledRequiredFields()) { - const dataKey = (action as APIAction)?.getDataKey() || "CometChatData" - let payload: any = (action as APIAction).getPayload() || {} - if (!(typeof payload === "object")) { - payload = {} - } - payload[dataKey] = { ...bodyData }; + let uiKitSettings = CometChatUIKit.uiKitSettings; + let body = { + appID: uiKitSettings.appId, + trigger: "ui_message_interacted", + region: uiKitSettings.region, + payload: {}, + data: {}, + }; + let payload: any = (action as APIAction).getPayload() || {}; + body.payload = payload; + + body.data = { + messageId: message.id, + conversationId: conversation.conversationId, + receiver: message.getSender().getUid(), + messageType: CometChatUiKitConstants.MessageTypeConstants.form, + messageCategory: + CometChatUiKitConstants.MessageCategoryConstants.interactive, + receiverType: message?.getReceiverType(), + interactedBy: loggedInUser.uid, + interactionTimezoneCode: currentTimeZoneRef.current, + sender: loggedInUser.uid, + formData: bodyData + }; + setLoaderButton(elementId) CometChatNetworkUtils.fetcher({ url: action.getURL(), method: (action as APIAction).getMethod() || HTTPSRequestMethods.POST, - body: { ...payload, cometchatSenderUid: loggedInUser?.['uid'] || "" }, + body, headers: (action as APIAction).getHeaders(), }) .then((response) => { @@ -442,18 +509,32 @@ export const CometChatFormBubble = memo((props: CometChatFormBubbleInterface) => : <View style={{ backgroundColor: backgroundColor, - padding: 10, borderRadius: borderRadius, ...border + borderRadius: borderRadius, ...border }}> - {Boolean(message.getTitle()) && <Text - style={[{ marginBottom: 15, marginTop: 10, color: titleColor }, titleFont]} - >{message.getTitle()}</Text>} - - { - message.getFormFields().map((item) => { - return renderField(item) - }) - } - {message.getSubmitElement() && _renderButton(message.getSubmitElement() as ButtonElement, true)} + {Boolean(message.getTitle()) && + <View style={{ + padding: 15, + borderBottomWidth: 0.6, + borderBottomColor: theme.palette.getAccent200(), + }}> + <Text + style={[ + { + color: titleColor, + }, + titleFont, + ]} + >{message.getTitle()}</Text> + </View> + } + <View style={{padding: 12}}> + { + message.getFormFields().map((item) => { + return renderField(item) + }) + } + {message.getSubmitElement() && _renderButton(message.getSubmitElement() as ButtonElement, true)} + </View> </View> } </View> diff --git a/src/shared/views/CometChatFormBubble/FormBubbleStyle.tsx b/src/shared/views/CometChatFormBubble/FormBubbleStyle.tsx index 9479801..0b2cfb1 100644 --- a/src/shared/views/CometChatFormBubble/FormBubbleStyle.tsx +++ b/src/shared/views/CometChatFormBubble/FormBubbleStyle.tsx @@ -1,6 +1,7 @@ import { BaseStyle, FontStyle } from "../../base"; import { ButtonStyleInterface } from "../CometChatButton"; import { CheckBoxStyleInterface } from "../CometChatCheckBox/CheckBoxStyle"; +import { DatePickerStyleInterface } from "../CometChatDateTimePicker"; import { DropDownStyleInterface } from "../CometChatDropDown/DropDownStyle"; import { LabelStyleInterface } from "../CometChatLabel/LabelStyle"; import { QuickViewStyleInterface } from "../CometChatQuickView/QuickViewStyle"; @@ -17,6 +18,7 @@ export interface FormBubbleStyleInterface extends BaseStyle { goalCompletionTextFont?: FontStyle; goalCompletionTextColor?: string; textInputStyle?: TextInputStyleInterface; + datePickerStyle?: DatePickerStyleInterface; quickViewStyle?: QuickViewStyleInterface; radioButtonStyle?: RadioButtonStyleInterface; checkboxStyle?: CheckBoxStyleInterface; @@ -35,6 +37,7 @@ export class FormBubbleStyle extends BaseStyle { goalCompletionTextFont?: FontStyle; goalCompletionTextColor?: string; textInputStyle?: TextInputStyleInterface; + datePickerStyle?: DatePickerStyleInterface; quickViewStyle?: QuickViewStyleInterface; radioButtonStyle?: RadioButtonStyleInterface; checkboxStyle?: CheckBoxStyleInterface; @@ -57,6 +60,7 @@ export class FormBubbleStyle extends BaseStyle { goalCompletionTextFont, goalCompletionTextColor, textInputStyle, + datePickerStyle, quickViewStyle, radioButtonStyle, checkboxStyle, @@ -80,6 +84,7 @@ export class FormBubbleStyle extends BaseStyle { this.goalCompletionTextFont = goalCompletionTextFont; this.goalCompletionTextColor = goalCompletionTextColor; this.textInputStyle = textInputStyle; + this.datePickerStyle = datePickerStyle; this.quickViewStyle = quickViewStyle; this.radioButtonStyle = radioButtonStyle; this.checkboxStyle = checkboxStyle; diff --git a/src/shared/views/CometChatLabel/CometChatLabel.tsx b/src/shared/views/CometChatLabel/CometChatLabel.tsx index 184928a..ebd89f2 100644 --- a/src/shared/views/CometChatLabel/CometChatLabel.tsx +++ b/src/shared/views/CometChatLabel/CometChatLabel.tsx @@ -18,7 +18,7 @@ const CometChatLabel = (props: CometChatCardBubbleInterface) => { labelFont, } = _style; return ( - <View style={{ padding: 10 }}> + <View style={{ padding: 10, paddingBottom: 12 }}> <Text style={[labelFont, { color: labelColor }]}>{text}</Text> </View> ) diff --git a/src/shared/views/CometChatRadioButton/CometChatRadioButton.tsx b/src/shared/views/CometChatRadioButton/CometChatRadioButton.tsx index cc8cb8e..2a0a119 100644 --- a/src/shared/views/CometChatRadioButton/CometChatRadioButton.tsx +++ b/src/shared/views/CometChatRadioButton/CometChatRadioButton.tsx @@ -40,10 +40,10 @@ const CometChatRadioButton = (props: CometChatRadioButtonInterface) => { } = _style; return ( - <View style={{ marginVertical: 10 }}> - <Text style={[titleFont, { color: titleColor }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> + <View style={{ marginBottom: 12 }}> + <Text style={[titleFont, { color: titleColor, marginBottom: 4 }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> {data.getOptions().map((option, index) => ( - <View key={index} style={{ flexDirection: "row", alignItems: "center", marginTop: 3 }}> + <View key={index} style={{ flexDirection: "row", alignItems: "center", marginVertical: 1.5 }}> <TouchableOpacity style={{ height: 20, width: 20, borderRadius: 20, alignItems: "center", justifyContent: "center", backgroundColor: selectedOption !== option.getValue() ? inactiveBackgroundColor : undefined, borderWidth: selectedOption === option.getValue() ? borderWidth + 1 : borderWidth, borderColor: showError ? theme.palette.getError() : borderColor, marginRight: 5, diff --git a/src/shared/views/CometChatSchedulerBubble/CometChatSchedulerBubble.tsx b/src/shared/views/CometChatSchedulerBubble/CometChatSchedulerBubble.tsx new file mode 100644 index 0000000..d120ff5 --- /dev/null +++ b/src/shared/views/CometChatSchedulerBubble/CometChatSchedulerBubble.tsx @@ -0,0 +1,1500 @@ +import React, { useContext, useEffect, useRef, useState, memo } from "react"; +import { + ActivityIndicator, + Image, + Text, + TouchableOpacity, + View, + NativeModules, +} from "react-native"; +import { CometChatContextType } from "../../base"; +import { CometChatContext } from "../../CometChatContext"; +import { SchedulerMessage } from "../../modals/InteractiveData/InteractiveMessage"; +import { SchedulerBubbleStyles } from "./styles"; +import { CometChatAvatar } from "../CometChatAvatar"; +import { styles } from "./styles"; +import { + SingleDateSelectionCalendar, + DefaultTheme, +} from "../../libs/CometChatCalendar"; +import { ICONS } from "../../assets/images"; +import { CometChatTimeSlotSelector } from "../CometChatTimeSlotSelector/CometChatTimeSlotSelector"; +import { CometChatButton } from "../CometChatButton"; +import icsToJson from "../../utils/icsToJson"; +import { APIAction, ButtonElement } from "../../modals"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; +import { CometChatNetworkUtils } from "../../utils/NetworkUtils"; +import { HTTPSRequestMethods } from "../../constants/UIKitConstants"; +import { DateTime } from "../../libs/luxon/src/luxon"; +import { + getMinSlotsFromRange, + getFormatedDateString, + convertDate, + convertToLocalTimeZone, + convertToATimeZone, + addMinutes, + convert24to12, +} from "../../utils/SchedulerUtils"; +import CometChatQuickView from "../CometChatQuickView/CometChatQuickView"; +import { localize } from "../../resources"; +import { CometChatUIKit } from "../../CometChatUiKit"; +import { CometChatUiKitConstants } from "../.."; + +const { TimeZoneCodeManager } = NativeModules; +export interface CometChatSchedulerBubbleInterface { + schedulerMessage: SchedulerMessage; + style?: SchedulerBubbleStyles; + onScheduleClick?: (data: SchedulerMessage) => void; +} +enum componentEnum { + loading, + quickSelect, + calendar, + timeSlot, + noTimeSlot, + schedule, + interacted, +} +enum slotErrorEnum { + noError, + error, + noSlot, +} +const dateFormats = { + date: "yyyy-MM-dd", + time: "HHmm", +}; + +interface anyObject { + [key: string]: any; +} +export const CometChatSchedulerBubble = memo( + (props: CometChatSchedulerBubbleInterface) => { + const { theme } = useContext<CometChatContextType>(CometChatContext); + const { schedulerMessage, style, onScheduleClick } = props; + const message = SchedulerMessage.fromJSON(schedulerMessage); + let interactiveData = message.getInteractiveData(); + + const [allowInteraction, setAllowInteraction] = useState<boolean>(true); + const [slotError, setSlotError] = useState<slotErrorEnum>( + slotErrorEnum.noError + ); + const [currentComp, setCurrentComp] = useState<anyObject>({ + current: componentEnum.loading, + previous: componentEnum.loading, + }); + const [selectedDate, setSelectedDate] = useState<string>( + new DateTime(new Date()).toFormat(dateFormats.date) + ); + + const [scheduleLoading, setScheduleLoading] = useState<boolean>(false); + const [dateAvailablity, setDateAvailablity] = useState<anyObject>({}); + const [selectedDateObj, setSlectedDateObj] = useState<anyObject>({}); + const [compDimensions, setCompDimensions] = useState<anyObject>({}); + const [selectedSlotState, setSelectedSlotState] = useState<anyObject>({}); + const [loggedInUser, setLoggedInUser] = useState<CometChat.User>({}); + const tempQuickSelectRange = useRef<any[]>([]); + const [quickAvailbleSlots, setQuickAvailbleSlots] = useState<any[]>([]); + const [availableRange, setAvailableRange] = useState<anyObject>({}); + const availablityObjRef = useRef<anyObject>({}); + const availablityObjBeforeSlots = useRef<anyObject>({}); + const currentTimeZoneRef = useRef<string>(""); + + useEffect(() => { + setCompDimensions(calcDimensions(currentComp.current)); + }, [currentComp]); + + const icsFileData = useRef({}); + + const getDayFomDate = (date) => { + let day = DateTime.fromFormat(date, "yyyy-MM-dd").toFormat("EEEE"); + return day; + }; + + const getConvertedAvailablityObjectForDate = (date, day, newObj) => { + let newAvailabilityObj = availablityObjRef.current; + if (!newAvailabilityObj || !newAvailabilityObj.availability) return; + newAvailabilityObj?.availability[day.toLowerCase()]?.forEach((item) => { + let from = (convertToATimeZone( + `${date} ${item.from}`, + newAvailabilityObj.timeZoneCode, + "yyyy-MM-dd HHmm", + "fromFormat", + "yyyy-MM-dd HHmm" + ) as string).split(" "); + let startDate = from[0]; + let startTime = from[1]; + let to = (convertToATimeZone( + `${date} ${item.to}`, + newAvailabilityObj.timeZoneCode, + "yyyy-MM-dd HHmm", + "fromFormat", + "yyyy-MM-dd HHmm" + ) as string).split(" "); + + let endDate = to[0]; + let endTime = to[1]; + if (endTime !== "2359" && endTime?.slice(-1) === "9") { + endTime = addMinutes(`${endTime.slice(0, 2)}:${endTime.slice(2)}`, 1); + } + if (startDate === endDate) { + if (newObj[startDate]) { + newObj[startDate] = { + day: getDayFomDate(startDate), + date: startDate, + availability: [ + ...newObj[startDate]["availability"], + { from: startTime, to: endTime }, + ], + }; + } else { + newObj[startDate] = { + day: getDayFomDate(startDate), + date: startDate, + availability: [{ from: startTime, to: endTime }], + }; + } + } else { + if (newObj[startDate]) { + newObj[startDate] = { + day: getDayFomDate(startDate), + date: startDate, + availability: [ + ...newObj[startDate]["availability"], + { from: startTime, to: "2359" }, + ], + }; + } else { + newObj[startDate] = { + day: getDayFomDate(startDate), + date: startDate, + availability: [{ from: startTime, to: "2359" }], + }; + } + if (newObj[endDate]) { + newObj[endDate] = { + day: getDayFomDate(endDate), + date: endDate, + availability: [ + ...newObj[endDate]["availability"], + { from: "0000", to: endTime }, + ], + }; + } else { + newObj[endDate] = { + day: getDayFomDate(endDate), + date: endDate, + availability: [{ from: "0000", to: endTime }], + }; + } + } + }); + }; + + const onSelectDate = async (date) => { + setSelectedDate(date); + let newObj = {}; + let previousDate = getNextFormatedDate(date, true); + let previousDay = DateTime.fromFormat( + previousDate, + dateFormats.date + ).toFormat("EEEE"); + getConvertedAvailablityObjectForDate(previousDate, previousDay, newObj); + let day = DateTime.fromFormat(date, dateFormats.date).toFormat("EEEE"); + getConvertedAvailablityObjectForDate(date, day, newObj); + let nextDate = getNextFormatedDate(date); + let nextDay = DateTime.fromFormat(nextDate, dateFormats.date).toFormat( + "EEEE" + ); + getConvertedAvailablityObjectForDate(nextDate, nextDay, newObj); + + if (newObj[date]) { + let availabilityObjWithTimeSlots = { ...newObj }; + let range = await getMeetingSlots({ + date, + availabilityObject: availabilityObjWithTimeSlots, + }); + if (range.length) { + let lastSlot: string = range[range.length - 1]; + + let lastSlotMinutes = + Number(lastSlot.slice(0, 2)) * 60 + Number(lastSlot.slice(3)); + + if ( + lastSlotMinutes < 1439 && + lastSlotMinutes + interactiveData.duration * 2 > 1440 + ) { + let nextDate = getNextFormatedDate(date); + + let nextRange = await getMeetingSlots({ + date: nextDate, + availabilityObject: availabilityObjWithTimeSlots, + }); + + if (nextRange[0] === "00:00") { + let nextSlotInMinutes = + lastSlotMinutes + interactiveData.duration + 1; + let hours = Math.floor(nextSlotInMinutes / 60); + let minutes = (nextSlotInMinutes - 1) % 60; + if (hours <= 23) + range.push( + `${String(hours).padStart(2, "0")}:${String(minutes).padStart( + 2, + "0" + )}` + ); + } + } + availabilityObjWithTimeSlots[date] = { + ...availabilityObjWithTimeSlots[date], + availability: range, + }; + + setDateAvailablity(availabilityObjWithTimeSlots[date]); + setCurrentComp((prev) => ({ + current: componentEnum.timeSlot, + previous: prev.current, + })); + } else { + setSlectedDateObj({ date, day }); + setCurrentComp((prev) => ({ + current: componentEnum.noTimeSlot, + previous: prev.current, + })); + } + } else { + setSlectedDateObj({ date, day }); + setCurrentComp((prev) => ({ + current: componentEnum.noTimeSlot, + previous: prev.current, + })); + } + }; + + const getAvailableDays = ({ + startDate, + endDate, + availability, + timeZoneCode, + }) => { + let initialDate = new Date(startDate); + let finalDate = new Date(endDate); + + availablityObjRef.current = { + startDate, + endDate, + availability, + timeZoneCode, + }; + setAvailableRange({ initialDate, finalDate }); + + getQuickSelectSlots(new Date()); + }; + + const combineAvailability = (availability) => { + if (availability && availability.length) { + // Sort availability array + availability.sort((a, b) => a.from.localeCompare(b.from)); + + let combinedAvailability = [availability[0]]; + for (let i = 1; i < availability.length; i++) { + let prev = combinedAvailability[combinedAvailability.length - 1]; + let curr = availability[i]; + if (prev.to === curr.from) { + // If the previous 'to' matches with current 'from', combine them + prev.to = curr.to; + } else { + // If not, add the current block to the result + combinedAvailability.push(curr); + } + } + + return combinedAvailability; + } + return []; + }; + + const getMeetingSlots = ({ + date, + availabilityObject, + blockedSlots, + }: any): Promise<string[]> => { + return new Promise((resolve, reject) => { + let range = []; + combineAvailability(availabilityObject[date]?.availability); + if ( + availabilityObject[date]?.availability?.length && + availabilityObject[date]?.availability[0]?.from + ) { + let newObj = JSON.parse(JSON.stringify(availabilityObject)); + availablityObjBeforeSlots.current[date] = { + ...newObj[date], + }; + } + + availabilityObject[date]?.availability?.forEach((item) => { + let icsSelectedSlots = icsFileData.current[date]; + let selectedSlots = []; + if (blockedSlots) { + selectedSlots = blockedSlots; + } + icsSelectedSlots?.forEach((item) => { + if (item.startDate === item.endDate) { + selectedSlots.push({ + startTime: item.startTime, + endTime: item.endTime, + }); + } else { + // managed if there is a meeting which ends next day + selectedSlots.push({ + startTime: item.startTime, + endTime: "2359", + }); + if (item.endTime == "0000") return; + if (icsFileData.current[item.endDate]) + icsFileData.current[item.endDate] = [ + ...icsFileData.current[item.endDate], + { + startDate: item.startDate, + endDate: item.startDate, + startTime: "0000", + endTime: item.endTime, + }, + ]; + else + icsFileData.current[item.endDate] = [ + { + startDate: item.endDate, + endDate: item.endDate, + startTime: "0000", + endTime: item.endTime, + }, + ]; + } + }); + + // get slots for a particular range + range = [ + ...range, + ...getMinSlotsFromRange({ + startTime: item.from, + endTime: item.to, + selectedSlots, + slotDuration: interactiveData.duration ?? 30, + bufferDuration: interactiveData?.bufferTime ?? 0, + }), + ]; + }); + resolve(range); + }); + }; + + const getNextQuickSlots = async (date, newQuickObj, i = 0) => { + if (date && date !== "Invalid DateTime" && i < 30) { + let nextDate = getNextFormatedDate(date); + let nextday = DateTime.fromFormat(nextDate, dateFormats.date).toFormat( + "EEEE" + ); + getConvertedAvailablityObjectForDate(nextDate, nextday, newQuickObj); + + let nextRange = await getMeetingSlots({ + date: nextDate, + availabilityObject: newQuickObj, + }); + if (newQuickObj[nextDate]) + newQuickObj[nextDate].availability = nextRange; + // Ensure nextRange is an Array + if (!Array.isArray(nextRange)) { + nextRange = []; + } + tempQuickSelectRange.current = [ + ...tempQuickSelectRange.current, + ...nextRange, + ]; + + if (tempQuickSelectRange.current.length < 2) { + return await getNextQuickSlots(nextDate, newQuickObj, i + 1); + } + return nextRange; + } + }; + + const getQuickSelectSlots = async (date) => { + let day = DateTime.fromJSDate(date).toFormat("EEEE"); + date = DateTime.fromJSDate(date).toFormat(dateFormats.date); + + let newQuickObj = {}; + getConvertedAvailablityObjectForDate(date, day, newQuickObj); + let blockedSlots = [ + { + startTime: "0000", + endTime: `${new Date().getHours()}${new Date().getMinutes()}`, + }, + ]; + let range = await getMeetingSlots({ + date, + availabilityObject: newQuickObj, + blockedSlots, + }); + + tempQuickSelectRange.current = range; + if (range.length && newQuickObj[date]) { + newQuickObj[date].availability = range; + } + // Ensure range is an Array + if (!Array.isArray(range)) { + range = []; + } + + if (range.length < 2) await getNextQuickSlots(date, newQuickObj); + + let quickSlotsArray = []; + + Object.entries(newQuickObj).forEach(([key, value]: [string, any]) => { + value.availability.forEach((item) => { + quickSlotsArray.push({ + slot: item, + date: value.date, + day: value.day, + }); + }); + }); + + setQuickAvailbleSlots(quickSlotsArray); + setCurrentComp((prev) => ({ + current: componentEnum.quickSelect, + previous: prev.current, + })); + }; + + const getICSFile = () => { + return new Promise((resolve, reject) => { + if (interactiveData.icsFileUrl) + fetch(interactiveData.icsFileUrl) + .then((response) => { + if (response.status === 200) { + return { response, status: response.status }; + } + return { status: response.status }; + }) + .then(async (res) => { + // let text = response?.text(); + // Converted ICS to JSON + if (res.status === 200) { + let text = await res.response.text(); + let jcalData = icsToJson(text); + // Restructure the JSON to make it more easy to assign to calendar dates + let finalJcalData = {}; + jcalData.forEach((item, index) => { + let startDate = convertToLocalTimeZone( + convertDate(item.startDate), + item.tzid ? item.tzid : "utc", + dateFormats.date + ); + let startTime = convertToLocalTimeZone( + convertDate(item.startDate), + item.tzid ? item.tzid : "utc", + dateFormats.time + ); + let endDate = convertToLocalTimeZone( + convertDate(item.endDate), + item.tzid ? item.tzid : "utc", + dateFormats.date + ); + let endTime = convertToLocalTimeZone( + convertDate(item.endDate), + item.tzid ? item.tzid : "utc", + dateFormats.time + ); + let finalObject = { + ...item, + startDate, + startTime, + endDate, + endTime, + }; + if (startDate === endDate) { + if (finalJcalData[startDate]) { + finalJcalData[startDate] = [ + ...finalJcalData[startDate], + finalObject, + ]; + } else finalJcalData[startDate] = [finalObject]; + } else { + let nextDate = getNextFormatedDate(startDate); + if (endDate !== nextDate) { + finalJcalData[startDate] = [ + { + ...finalObject, + startDate: startDate, + endDate: startDate, + endTime: "2359", + }, + ]; + while (endDate !== nextDate) { + if (finalJcalData[nextDate]) { + finalJcalData[nextDate] = [ + ...finalJcalData[nextDate], + { + ...finalObject, + endTime: "2359", + endDate: nextDate, + startDate: nextDate, + startTime: "0000", + }, + ]; + } else + finalJcalData[nextDate] = [ + { + ...finalObject, + endTime: "2359", + startDate: nextDate, + endDate: nextDate, + startTime: "0000", + }, + ]; + + nextDate = getNextFormatedDate(nextDate); + } + if (endDate === nextDate) { + finalJcalData[nextDate] = [ + { + ...finalObject, + startDate: nextDate, + endDate: nextDate, + startTime: "0000", + }, + ]; + } + } else { + if (finalJcalData[startDate]) { + finalJcalData[startDate] = [ + ...finalJcalData[startDate], + { + ...finalObject, + endTime: "2359", + endDate: startDate, + }, + ]; + } else + finalJcalData[startDate] = [ + { + ...finalObject, + endTime: "2359", + endDate: startDate, + }, + ]; + if (finalJcalData[endDate]) { + finalJcalData[endDate] = [ + ...finalJcalData[endDate], + { + ...finalObject, + startTime: "0000", + startDate: endDate, + }, + ]; + } else + finalJcalData[endDate] = [ + { + ...finalObject, + startTime: "0000", + startDate: endDate, + }, + ]; + } + } + }); + icsFileData.current = finalJcalData; + resolve(finalJcalData); + } else { + reject(res); + } + }) + .catch((err) => { + console.log("Error :: ", err); + reject(err); + }); + else reject(); + }); + }; + + useEffect(() => { + CometChat.getLoggedinUser() + .then((u) => { + TimeZoneCodeManager.getCurrentTimeZone((timeZone) => { + currentTimeZoneRef.current = timeZone; + }); + setLoggedInUser(u); + if ( + u?.getUid() === schedulerMessage?.getSender()?.getUid() && + schedulerMessage?.data?.allowSenderInteraction == false + ) { + setAllowInteraction(false); + } + getICSFile(); + if (interactiveData) { + getAvailableDays({ + availability: interactiveData.availability, + startDate: convertToATimeZone( + `${interactiveData.dateRangeStart}T00:00:00`, + interactiveData.timezoneCode, + dateFormats.date, + "fromISO" + ), + endDate: convertToATimeZone( + `${interactiveData.dateRangeEnd}T23:59:00`, + interactiveData.timezoneCode, + dateFormats.date, + "fromISO" + ), + timeZoneCode: interactiveData.timezoneCode, + }); + } + }) + .catch((e) => { + console.log("Error while getting loggedInUser"); + setLoggedInUser(null); + }); + }, []); + + const QuickSelectView = () => { + return ( + <View> + <AvatarView + avatar={ + interactiveData?.avatarUrl || message?.getSender()?.getAvatar() + } + title={ + interactiveData?.title || + `${localize("MEET_WITH")} ${message?.getSender()?.getName()}` + } + /> + <View style={styles.quickContTimeContainer}> + {quickAvailbleSlots.slice(0, 3).map((item) => { + return ( + <TimeBoxView + text={`${( + item?.day?.charAt(0)?.toUpperCase() + item?.day?.slice(1) + ).slice(0, 3)}, ${DateTime.fromFormat( + item.date, + "yyyy-MM-dd" + ).toFormat("LLL dd")} at ${convert24to12(item.slot)}`} + item={item} + /> + ); + })} + <Text + style={[ + theme.typography.subtitle5, + { color: theme.palette.getAccent600(), lineHeight: 22 }, + ]} + > + {interactiveData?.duration} + {localize("MIN_MEETING")} • {interactiveData?.timezoneCode} + </Text> + </View> + <TouchableOpacity + style={styles.quickContMore} + onPress={() => { + if (allowInteraction) + setCurrentComp((prev) => ({ + current: componentEnum.calendar, + previous: prev.current, + })); + }} + > + <Text + style={[ + theme.typography.subtitle4, + { + color: allowInteraction + ? theme.palette.getPrimary() + : theme.palette.getAccent800(), + }, + ]} + > + {localize("MORE_TIMES")} + </Text> + </TouchableOpacity> + </View> + ); + }; + const TimeBoxView = ({ text, item }) => { + return ( + <TouchableOpacity + key={text} + style={[ + styles.timeBoxContainer, + { + borderColor: allowInteraction + ? theme.palette.getPrimary() + : theme.palette.getAccent800(), + }, + style?.suggestedTimeBorder ?? {}, + { + backgroundColor: + style?.suggestedTimeBackground ?? + theme.palette.getBackgroundColor(), + }, + ]} + onPress={() => { + if (allowInteraction) { + setSelectedSlotState({ + startTime: `${item?.slot?.replace(":", "")}`, + endTime: addMinutes(item.slot, interactiveData.duration), + selectedDate: item?.date, + selectedDay: item?.day, + }); + setCurrentComp((prev) => ({ + current: componentEnum.schedule, + previous: prev.current, + })); + } + }} + > + <Text + style={[ + theme.typography.subtitle2, + style?.suggestedTimeTextFont ?? {}, + { + color: allowInteraction + ? style?.suggestedTimeTextColor ?? theme.palette.getPrimary() + : theme.palette.getAccent800(), + }, + ]} + > + {text} + </Text> + </TouchableOpacity> + ); + }; + + const AvatarView = (props) => { + const { avatar, title, subTitle, onBackPress } = props; + if ( + currentComp.current === componentEnum.quickSelect || + currentComp.current === componentEnum.loading + ) + return ( + <View style={styles.quickAvatarContainer}> + <CometChatAvatar + name={message?.getSender()?.getName()} + image={{ + uri: avatar, + }} + style={{ + outerView: { borderWidth: 0 }, + ...styles.avatarCont, + ...(style?.avatarStyle ? style.avatarStyle : {}), + }} + /> + <Text + style={[ + theme.typography.heading3, + style?.titleTextFont ?? {}, + { + color: + style?.titleTextColor ?? theme?.palette?.getAccent800(), + textAlign: "center", + }, + ]} + > + {title} + </Text> + </View> + ); + return ( + <View style={styles.calendarAvatarContainer}> + <TouchableOpacity + style={{ paddingHorizontal: 6 }} + onPress={() => { + if (slotError !== slotErrorEnum.noError) + setSlotError(slotErrorEnum.noError); + onBackPress(); + }} + > + <Image source={ICONS.BACK_ARROW} style={styles.calendarBackArrow} /> + </TouchableOpacity> + {Boolean(avatar) && ( + <CometChatAvatar + name={message?.getSender()?.getName()} + style={{ + outerView: { borderWidth: 0 }, + ...styles.avatarCont, + ...(style?.avatarStyle ? style.avatarStyle : {}), + }} + image={{ + uri: avatar, + }} + /> + )} + <View + style={{ paddingHorizontal: Boolean(avatar) ? 10 : 0, flex: 1 }} + > + {Boolean(title) && ( + <Text + numberOfLines={1} + ellipsizeMode="tail" + style={[ + theme.typography.name, + style?.titleTextFont ?? {}, + { + color: + style?.titleTextColor ?? theme?.palette?.getAccent800(), + flex: 1, + }, + ]} + > + {title} + </Text> + )} + {Boolean(subTitle) && ( + <View + style={{ + alignItems: "center", + flexDirection: "row", + height: 28, + }} + > + <Image + source={ICONS.CLOCK} + style={{ + height: 18, + width: 18, + tintColor: theme.palette.getAccent600(), + }} + /> + <Text + style={[ + theme.typography.subtitle1, + style?.summaryTextFont ?? {}, + { + color: + style?.summaryTextColor ?? + theme?.palette?.getAccent600(), + }, + ]} + > + {" "} + {subTitle} + </Text> + </View> + )} + </View> + </View> + ); + }; + const getNextFormatedDate = (date, previous = false) => { + let nextDate = getFormatedDateString({ + date: DateTime.fromJSDate( + new Date( + new Date(date).setDate( + previous + ? new Date(date).getDate() - 1 + : new Date(date).getDate() + 1 + ) + ) + ).toMillis(), + format: dateFormats.date, + }); + return nextDate; + }; + + const CalendarView = () => { + let initVisibleDate = selectedDate; + if (new Date(selectedDate) < new Date(availableRange.initialDate)) { + initVisibleDate = availableRange.initialDate; + } + return ( + <View> + <AvatarView + avatar={ + interactiveData?.avatarUrl || message?.getSender()?.getAvatar() + } + title={ + interactiveData?.title || + `${localize("MEET_WITH")} ${message?.getSender()?.getName()}` + } + subTitle={`${interactiveData?.duration} min`} + onBackPress={() => { + setCurrentComp((prev) => ({ + current: componentEnum.quickSelect, + previous: prev.current, + })); + }} + /> + <View style={styles.calendarSelectDayContainer}> + <Text + style={[ + theme.typography.text1, + { color: theme?.palette?.getAccent900() }, + ]} + > + {localize("SELECT_DAY")} + </Text> + </View> + <View style={{ padding: 5 }}> + <SingleDateSelectionCalendar + initVisibleDate={initVisibleDate} + showExtraDates={true} + testID={"calendar-1"} + minDate={availableRange.initialDate} + maxDate={availableRange.finalDate} + onSelectDate={onSelectDate} + selectedDate={selectedDate} + // ArrowComponent={()} + theme={{ + ...DefaultTheme, + calendarContainer: [ + DefaultTheme.calendarContainer, + { backgroundColor: "transparent" }, + ], + normalArrowImage: [ + DefaultTheme.normalArrowImage, + { tintColor: "grey" }, + ], + selectedMonthText: [ + DefaultTheme.selectedMonthText, + { + color: theme?.palette?.getPrimary(), + }, + ], + selectedDayContainer: [ + DefaultTheme.selectedDayContainer, + { + borderRadius: 5, + backgroundColor: theme?.palette?.getPrimary(), + }, + ], + }} + /> + <View style={styles.calendarTimeZoneContainer}> + <Text + style={[ + theme.typography.subtitle2, + { color: theme.palette.getAccent900() }, + ]} + > + <Image + source={ICONS.EARTH} + style={{ + height: 15, + width: 15, + tintColor: theme.palette.getAccent900(), + }} + />{" "} + {interactiveData.timezoneCode} + </Text> + </View> + </View> + </View> + ); + }; + + const onSlotSelection = (startTime, endTime) => { + setSelectedSlotState({ + startTime, + endTime, + selectedDate: dateAvailablity?.date, + selectedDay: dateAvailablity?.day, + }); + setCurrentComp((prev) => ({ + current: componentEnum.schedule, + previous: prev.current, + })); + }; + + const NoTimeSlotsView = () => { + return ( + <View style={{ flex: 1 }}> + <AvatarView + avatar={ + interactiveData?.avatarUrl || message?.getSender()?.getAvatar() + } + title={ + interactiveData?.title || + `${localize("MEET_WITH")} ${message?.getSender()?.getName()}` + } + subTitle={`${interactiveData?.duration} min`} + onBackPress={() => { + setCurrentComp((prev) => ({ + current: componentEnum.calendar, + previous: prev.current, + })); + }} + /> + <View + style={{ + height: 45, + justifyContent: "center", + paddingHorizontal: 10, + }} + > + <Text + style={[ + theme.typography.subtitle1, + { + borderBottomWidth: 0.45, + borderBottomColor: "rgba(212, 213, 215, 1)", + color: theme.palette.getAccent800(), + lineHeight: 30, + }, + ]} + > + <Image + source={ICONS.CALENDAR} + style={{ height: 15, width: 15 }} + />{" "} + {selectedDateObj.date && selectedDateObj?.day + ? `${DateTime.fromFormat( + selectedDateObj.date, + dateFormats.date + ).toFormat("dd MMMM y")}, ${ + selectedDateObj?.day?.charAt(0)?.toUpperCase() + + selectedDateObj?.day?.slice(1) + }` + : ""} + </Text> + </View> + <Text + style={[ + theme.typography.text1, + { + color: theme.palette.getAccent800(), + marginLeft: 10, + marginBottom: 5, + marginTop: 2, + }, + ]} + > + {localize("SELECT_TIME")} + </Text> + <View style={styles.noTimeSlotTextContainer}> + <Image source={ICONS.CLOCK_ALERT} style={styles.noTimeSlotImage} /> + <Text + style={[ + theme.typography.subtitle1, + { + color: theme.palette.getAccent500(), + textAlign: "center", + }, + ]} + > + {localize("NO_TIME_SLOT_AVAILABLE")} + </Text> + </View> + <View style={styles.calendarTimeZoneContainer}> + <Text + style={[ + theme.typography.subtitle2, + { + color: theme.palette.getAccent900(), + marginBottom: 4, + marginRight: 2, + }, + ]} + > + <Image + source={ICONS.EARTH} + style={{ + height: 15, + width: 15, + tintColor: theme.palette.getAccent900(), + }} + />{" "} + {interactiveData.timezoneCode} + </Text> + </View> + </View> + ); + }; + + const TimeSlotView = () => { + return ( + <View style={{ flex: 1 }}> + <AvatarView + avatar={ + interactiveData?.avatarUrl || message?.getSender()?.getAvatar() + } + title={ + interactiveData?.title || + `${localize("MEET_WITH")} ${message?.getSender()?.getName()}` + } + subTitle={`${interactiveData?.duration} min`} + onBackPress={() => { + setCurrentComp((prev) => ({ + current: componentEnum.calendar, + previous: prev.current, + })); + }} + /> + <View + style={{ + height: 45, + justifyContent: "center", + paddingHorizontal: 10, + }} + > + <Text + style={[ + theme.typography.subtitle1, + { + borderBottomWidth: 0.45, + borderBottomColor: "rgba(212, 213, 215, 1)", + color: theme.palette.getAccent800(), + lineHeight: 30, + }, + ]} + > + <Image + source={ICONS.CALENDAR} + style={{ height: 15, width: 15 }} + />{" "} + {`${DateTime.fromFormat( + dateAvailablity.date, + dateFormats.date + ).toFormat("dd MMMM y")}, ${ + dateAvailablity?.day?.charAt(0)?.toUpperCase() + + dateAvailablity?.day?.slice(1) + }`} + </Text> + </View> + <CometChatTimeSlotSelector + timeFormat="12Hr" + duration={interactiveData.duration} + slots={dateAvailablity.availability} + onSelection={onSlotSelection} + timeZone={interactiveData.timezoneCode} + style={style?.timeSlotSelectorStyle} + slotSelected={selectedSlotState?.startTime} + /> + </View> + ); + }; + + async function _handleButtonClick(buttonData: ButtonElement) { + setScheduleLoading(true); + await getICSFile().catch((err) => { + console.log(err); + setSlotError(slotErrorEnum.error); + setScheduleLoading(false); + }); + let range = await getMeetingSlots({ + date: selectedSlotState.selectedDate, + availabilityObject: availablityObjBeforeSlots.current, + }); + let slotExists = range.find((item) => { + let startTime = selectedSlotState.startTime; + return `${startTime?.slice(0, 2)}:${startTime?.slice(2)}` === item; + }); + + if (!slotExists) { + setSlotError(slotErrorEnum.noSlot); + setScheduleLoading(false); + return; + } + let conversation = await CometChat.CometChatHelper.getConversationFromMessage( + message + ).then( + (conversation) => { + return conversation; + }, + (error) => { + console.log("Error while converting message object", error); + setSlotError(slotErrorEnum.error); + setScheduleLoading(false); + return undefined; + } + ); + if (!conversation) { + setSlotError(slotErrorEnum.error); + setScheduleLoading(false); + return; + } + let action = interactiveData.scheduleElement.action; + let buttonAction = APIAction.fromJSON(action); + let uiKitSettings = CometChatUIKit.uiKitSettings; + let body = { + appID: uiKitSettings.appId, + trigger: "ui_message_interacted", + region: uiKitSettings.region, + payload: {}, + data: {}, + }; + let payload: any = buttonAction.getPayload() || {}; + body.payload = payload; + + body.data = { + messageId: schedulerMessage.id, + conversationId: conversation.conversationId, + receiver: schedulerMessage.getSender().getUid(), + messageType: CometChatUiKitConstants.MessageTypeConstants.scheduler, + messageCategory: + CometChatUiKitConstants.MessageCategoryConstants.interactive, + schedulerData: { + meetStartAt: `${ + selectedSlotState.selectedDate + }T${selectedSlotState?.startTime?.slice( + 0, + 2 + )}:${selectedSlotState?.startTime?.slice(2)}`, + duration: interactiveData.duration, + }, + receiverType: schedulerMessage?.getReceiverType(), + interactedBy: loggedInUser.uid, + sender: loggedInUser.uid, + interactionTimezoneCode: currentTimeZoneRef.current, + interactedElementId: buttonData.getElementId(), + }; + onScheduleClick && onScheduleClick(message); + CometChatNetworkUtils.fetcher({ + url: buttonAction.getURL(), + method: buttonAction.getMethod() || HTTPSRequestMethods.POST, + body, + headers: buttonAction.getHeaders(), + }) + .then((response) => { + console.log("response", response.status, JSON.stringify(response)); + if (response.status == 200) { + setCurrentComp((prev) => ({ + current: componentEnum.interacted, + previous: prev.current, + })); + } else { + setSlotError(slotErrorEnum.error); + } + setScheduleLoading(false); + }) + .catch((error) => { + setScheduleLoading(false); + setSlotError(slotErrorEnum.error); + console.log("CometChatNetworkUtils.fetcher error", error); + }); + } + + const _renderButton = (data: ButtonElement, isSubmitElement?: boolean) => { + let buttonData = ButtonElement.fromJSON(data); + + function isDisabled() { + let isSender = message.getSender()?.getUid() == loggedInUser?.["uid"]; + let allowInteraction = isSender + ? message?.["data"]?.["allowSenderInteraction"] + : true; + + return !allowInteraction; + } + + return ( + <View style={{ opacity: isDisabled() ? 0.7 : 1, marginVertical: 5 }}> + <CometChatButton + isLoading={scheduleLoading} + onPress={ + isDisabled() ? () => {} : () => _handleButtonClick(buttonData) + } + text={ + slotError === slotErrorEnum.noSlot + ? localize("BOOK_NEW_SLOT") + : slotError === slotErrorEnum.error + ? localize("TRY_AGAIN_CAMEL") + : buttonData.getButtonText() + } + style={{ + backgroundColor: theme.palette.getPrimary(), + textColor: theme.palette.getBackgroundColor(), + borderRadius: 5, + height: 30, + }} + /> + </View> + ); + }; + const ScheduleView = () => { + return ( + <View> + <AvatarView + avatar={ + interactiveData?.avatarUrl || message?.getSender()?.getAvatar() + } + title={ + interactiveData?.title || + `${localize("MEET_WITH")} ${message?.getSender()?.getName()}` + } + subTitle={`${interactiveData?.duration} ${localize("MIN")}`} + onBackPress={() => { + setCurrentComp((prev) => ({ + current: prev.previous, + previous: prev.current, + })); + }} + /> + <View style={styles.scheduleSecContainer}> + <View style={styles.scheduleTimeContainer}> + <Image + source={ICONS.CALENDAR} + style={{ height: 18, width: 18, marginTop: 4, marginRight: 5 }} + /> + <Text + style={[ + theme.typography.subtitle1, + { + color: theme.palette.getAccent800(), + }, + ]} + > + {`${selectedSlotState?.startTime?.slice( + 0, + 2 + )}:${selectedSlotState?.startTime?.slice(2)}, ${ + selectedSlotState?.selectedDay?.charAt(0)?.toUpperCase() + + selectedSlotState?.selectedDay?.slice(1) + }, ${DateTime.fromFormat( + selectedSlotState.selectedDate, + dateFormats.date + ).toFormat("dd MMMM y")}`} + </Text> + </View> + <View style={styles.scheduleTimeZoneCont}> + <Image source={ICONS.EARTH} style={{ height: 18, width: 18 }} /> + <Text + style={[ + theme.typography.subtitle1, + { + color: theme.palette.getAccent800(), + paddingVertical: 10, + }, + ]} + > + {" "} + {interactiveData.timezoneCode} + </Text> + </View> + <View style={styles.scheduleErrorCont}> + {message && + _renderButton( + message?.getScheduleElement() as ButtonElement, + true + )} + {slotError !== slotErrorEnum.noError && ( + <Text + style={[ + theme.typography.subtitle5, + { + color: theme.palette.getError(), + textAlign: "center", + marginHorizontal: 15, + }, + ]} + > + {slotError === slotErrorEnum.noSlot + ? localize("TIME_SLOT_UNAVAILABLE") + : localize("SOMETHING_WRONG")} + </Text> + )} + </View> + </View> + </View> + ); + }; + + const LoadingView = () => { + return ( + <View style={{ flex: 1 }}> + <AvatarView + avatar={ + interactiveData?.avatarUrl || message?.getSender()?.getAvatar() + } + title={ + interactiveData?.title || + `${localize("MEET_WITH")} ${message?.getSender()?.getName()}` + } + /> + <View style={styles.lodingContainer}> + <ActivityIndicator + size="small" + color={theme?.palette.getAccent()} + /> + </View> + </View> + ); + }; + + const InteractedView = () => { + return ( + <View style={{ padding: 5 }}> + <CometChatQuickView + title={message.getTitle()} + subtitle={localize("MEETING_SCHEDULER")} + quickViewStyle={style?.quickViewStyle ?? {}} + /> + <Text + style={[ + { + marginTop: 10, + color: + style?.goalCompletionTextColor ?? + theme.palette.getAccent800(), + }, + style?.goalCompletionTextFont ?? {}, + ]} + > + {message.getGoalCompletionText() || + localize("FORM_COMPLETION_MESSAGE")} + </Text> + </View> + ); + }; + + const CurrentCommponent = () => { + switch (currentComp.current) { + case componentEnum.loading: + return <LoadingView />; + case componentEnum.quickSelect: + return <QuickSelectView />; + case componentEnum.calendar: + return <CalendarView />; + case componentEnum.noTimeSlot: + return <NoTimeSlotsView />; + case componentEnum.timeSlot: + return <TimeSlotView />; + case componentEnum.schedule: + return <ScheduleView />; + case componentEnum.interacted: + return <InteractedView />; + default: + return null; + } + }; + const calcDimensions = (currentComp) => { + switch (currentComp) { + case componentEnum.quickSelect: + return { width: 272 }; + case componentEnum.calendar: + return { width: 280 }; + case componentEnum.timeSlot: + return { width: 300 }; + case componentEnum.schedule: + return { width: 280 }; + case componentEnum.interacted: + return { width: 272 }; + case componentEnum.noTimeSlot: + return { width: 272 }; + default: + return { width: 272, height: 300 }; + } + }; + return ( + <View + style={[ + styles.mainContainer, + style?.border ?? {}, + { + backgroundColor: style?.backgroundColor ?? "transparent", + }, + compDimensions, + ]} + > + <CurrentCommponent /> + </View> + ); + } +); diff --git a/src/shared/views/CometChatSchedulerBubble/index.ts b/src/shared/views/CometChatSchedulerBubble/index.ts new file mode 100644 index 0000000..83f0425 --- /dev/null +++ b/src/shared/views/CometChatSchedulerBubble/index.ts @@ -0,0 +1,5 @@ +export { + CometChatSchedulerBubble, + CometChatSchedulerBubbleInterface, +} from "./CometChatSchedulerBubble"; +export { SchedulerBubbleStyles } from "./styles"; diff --git a/src/shared/views/CometChatSchedulerBubble/styles.ts b/src/shared/views/CometChatSchedulerBubble/styles.ts new file mode 100644 index 0000000..e4120cb --- /dev/null +++ b/src/shared/views/CometChatSchedulerBubble/styles.ts @@ -0,0 +1,92 @@ +import { StyleSheet } from "react-native"; +import { BorderStyleInterface, FontStyleInterface } from "../../base"; +import { AvatarStyleInterface } from "../CometChatAvatar"; +import { ButtonStyleInterface } from "../CometChatButton"; +import { TimeSlotSelectorStyles } from "../CometChatTimeSlotSelector/styles"; +import { QuickViewStyleInterface } from "../CometChatQuickView/QuickViewStyle"; +export interface SchedulerBubbleStyles { + // width, + // height, + border?: BorderStyleInterface; + backgroundColor?: string; + avatarStyle?: AvatarStyleInterface; + suggestedTimeTextFont?: FontStyleInterface; + suggestedTimeTextColor?: string; + suggestedTimeBackground?: string; + suggestedTimeBorder?: BorderStyleInterface; + // dateSelectorStyle; + timeSlotSelectorStyle?: TimeSlotSelectorStyles; + submitButtonStyle?: ButtonStyleInterface; + titleTextFont?: FontStyleInterface; + quickViewStyle?: QuickViewStyleInterface; + titleTextColor?: string; + summaryTextFont?: FontStyleInterface; + summaryTextColor?: string; + goalCompletionTextColor?: string; + goalCompletionTextFont?: FontStyleInterface; +} +export const styles = StyleSheet.create({ + mainContainer: { + borderWidth: 0.69, + borderColor: "rgba(212, 213, 215, 1)", + borderRadius: 10, + }, + quickAvatarContainer: { + justifyContent: "center", + alignItems: "center", + paddingVertical: 13, + borderBottomWidth: 1, + borderBottomColor: "rgba(212, 213, 215, 1)", + }, + avatarCont: { height: 48, width: 48, border: { borderWidth: 0 } }, + quickContTimeContainer: { paddingHorizontal: 20, paddingTop: 6 }, + quickContMore: { + alignItems: "center", + paddingTop: 12, + paddingBottom: 15, + justifyContent: "center", + }, + timeBoxContainer: { + justifyContent: "center", + alignItems: "center", + height: 30, + backgroundColor: "rgba(255, 255, 255, 1)", + borderWidth: 0.69, + borderRadius: 8, + marginTop: 10, + }, + calendarBackArrow: { height: 18, width: 18 }, + calendarAvatarContainer: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 8, + borderBottomWidth: 1, + borderBottomColor: "rgba(212, 213, 215, 1)", + }, + calendarSelectDayContainer: { paddingLeft: 10, paddingTop: 10 }, + calendarTimeZoneContainer: { + paddingTop: 5, + paddingBottom: 10, + alignItems: "flex-end", + paddingRight: 10, + }, + lodingContainer: { flex: 1, justifyContent: "center", alignItems: "center" }, + noTimeSlotTextContainer: { + justifyContent: "center", + paddingLeft: 10, + flex: 1, + alignItems: "center", + height: 250, + }, + noTimeSlotImage: { height: 30, width: 30, marginBottom: 20 }, + scheduleSecContainer: { paddingHorizontal: 10, paddingTop: 15 }, + scheduleTimeContainer: { flexDirection: "row", paddingRight: 15 }, + scheduleTimeZoneCont: { + flexDirection: "row", + alignItems: "center", + }, + scheduleErrorCont: { + width: "100%", + paddingBottom: 10, + }, +}); diff --git a/src/shared/views/CometChatSingleSelect/CometChatSingleSelect.tsx b/src/shared/views/CometChatSingleSelect/CometChatSingleSelect.tsx index 98fbd35..8abcc3e 100644 --- a/src/shared/views/CometChatSingleSelect/CometChatSingleSelect.tsx +++ b/src/shared/views/CometChatSingleSelect/CometChatSingleSelect.tsx @@ -21,7 +21,7 @@ const CometChatSingleSelect = (props: CometChatSingleSelectInterface) => { titleFont: theme.typography.subtitle1, titleColor: theme.palette.getAccent(), border: { borderWidth: 1, borderColor: theme.palette.getAccent200() }, - optionFont: theme.typography.title2, + optionFont: theme.typography.subtitle1, optionColorActive: theme.palette.getAccent900(), optionColorInactive: theme.palette.getAccent300(), ...style, @@ -37,9 +37,9 @@ const CometChatSingleSelect = (props: CometChatSingleSelectInterface) => { } = _style; return ( - <View style={{ marginVertical: 10 }}> - <Text style={[titleFont, { color: titleColor }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> - <View style={{ flex: 1, flexDirection: "row", marginTop: 3, flexWrap: "wrap" }}> + <View style={{ marginBottom: 12 }}> + <Text style={[titleFont, { color: titleColor, marginBottom: 4 }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> + <View style={{ flex: 1, flexDirection: "row", flexWrap: "wrap" }}> {data.getOptions().map((option, index) => ( <TouchableOpacity style={{ width: data.getOptions().length > 2 ? "100%" : "50%", diff --git a/src/shared/views/CometChatTextInput/CometChatTextInput.tsx b/src/shared/views/CometChatTextInput/CometChatTextInput.tsx index 8cf9b2a..a5a2f3b 100644 --- a/src/shared/views/CometChatTextInput/CometChatTextInput.tsx +++ b/src/shared/views/CometChatTextInput/CometChatTextInput.tsx @@ -46,16 +46,16 @@ const CometChatTextInput = (props: CometChatTextInputInterface) => { } return ( - <View style={{ marginBottom: 10 }}> - <Text style={[titleFont, { color: titleColor }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> + <View style={{ marginBottom: 12 }}> + <Text style={[titleFont, { color: titleColor, marginBottom: 4 }]}>{data.getLabel()}{!data.getOptional() && "*"}</Text> <TextInput // autoFocus={true} value={value} autoCorrect={false} onChangeText={_onChange} style={{ - borderColor: showError ? theme.palette.getError() : borderColor, paddingHorizontal: 5, paddingVertical: 8, - borderWidth: borderWidth, borderRadius: 5, marginTop: 5, + borderColor: showError ? theme.palette.getError() : borderColor, paddingHorizontal: 8, paddingVertical: 4.8, + borderWidth: borderWidth, borderRadius: 5 }} placeholder={data.getPlaceholder()} placeholderTextColor={placeholderColor} diff --git a/src/shared/views/CometChatTimeSlotSelector/CometChatTimeSlotSelector.tsx b/src/shared/views/CometChatTimeSlotSelector/CometChatTimeSlotSelector.tsx new file mode 100644 index 0000000..7ece775 --- /dev/null +++ b/src/shared/views/CometChatTimeSlotSelector/CometChatTimeSlotSelector.tsx @@ -0,0 +1,113 @@ +import React, { useContext, memo } from "react"; +import { Image, Text, TouchableOpacity, View } from "react-native"; +import { CometChatContextType } from "../../base"; +import { CometChatContext } from "../../CometChatContext"; +import { TimeSlotSelectorStyles, styles } from "./styles"; +import { ICONS } from "../../assets/images"; +import { addMinutes, convert24to12 } from "../../utils/SchedulerUtils"; + +export interface CometChatTimeSlotSelectorInterface { + style: TimeSlotSelectorStyles; + duration: number; + timeZone: string; + slotSelected: string; + timeFormat: "12Hr" | "24Hr"; + slots: string[]; + onSelection: (from: string, to: string) => void; +} +export const CometChatTimeSlotSelector = memo( + (props: CometChatTimeSlotSelectorInterface) => { + const { theme } = useContext<CometChatContextType>(CometChatContext); + const { + slots, + onSelection, + duration, + timeZone, + style, + slotSelected, + timeFormat = "12Hr", + } = props; + + const TimeSlot = ({ time }) => { + return ( + <TouchableOpacity + key={time} + style={[ + styles.timeSlotContainer, + slotSelected === time + ? { + backgroundColor: + style?.selectedSlotBackgroundColor ?? + theme.palette.getPrimary(), + } + : { + backgroundColor: + style?.slotBackgroundColor ?? + theme.palette.getBackgroundColor(), + }, + ]} + onPress={() => { + onSelection && + onSelection( + `${time.replace(":", "")}`, + addMinutes(time, duration) + ); + }} + > + <Text + style={[ + theme.typography.subtitle3, + style?.slotTextFont ?? {}, + { color: style?.slotTextColor ?? theme.palette.getAccent800() }, + ]} + > + {timeFormat === "12Hr" ? convert24to12(time) : time} + </Text> + </TouchableOpacity> + ); + }; + + return ( + <View style={styles.container}> + <Text + style={[ + theme.typography.text1, + { + color: theme.palette.getAccent800(), + marginLeft: 10, + marginBottom: 5, + }, + ]} + > + Select a Time + </Text> + <View style={styles.secContainer}> + {slots?.map((item) => { + return <TimeSlot time={item} />; + })} + </View> + <Text + style={[ + theme.typography.subtitle2, + { + color: theme.palette.getAccent900(), + marginLeft: 10, + paddingVertical: 10, + alignSelf: "flex-end", + paddingHorizontal: 10, + }, + ]} + > + <Image + source={ICONS.EARTH} + style={[ + styles.earthIcon, + { tintColor: theme.palette.getAccent900() }, + ]} + />{" "} + {timeZone} + </Text> + </View> + ); + } +); diff --git a/src/shared/views/CometChatTimeSlotSelector/index.ts b/src/shared/views/CometChatTimeSlotSelector/index.ts new file mode 100644 index 0000000..a2ab15c --- /dev/null +++ b/src/shared/views/CometChatTimeSlotSelector/index.ts @@ -0,0 +1,5 @@ +export { + CometChatTimeSlotSelector, + CometChatTimeSlotSelectorInterface, +} from "./CometChatTimeSlotSelector"; +export { TimeSlotSelectorStyles } from "./styles"; diff --git a/src/shared/views/CometChatTimeSlotSelector/styles.ts b/src/shared/views/CometChatTimeSlotSelector/styles.ts new file mode 100644 index 0000000..9566f0d --- /dev/null +++ b/src/shared/views/CometChatTimeSlotSelector/styles.ts @@ -0,0 +1,30 @@ +import { StyleSheet } from "react-native"; +import { FontStyleInterface } from "../../base"; +export interface TimeSlotSelectorStyles { + titleFont?: FontStyleInterface; + titleColor?: string; + // BaseStyle; + slotTextFont?: FontStyleInterface; + slotTextColor?: string; + slotBackgroundColor?: string; + selectedSlotBackgroundColor?: string; + selectedSlotTextColor?: string; +} +export const styles = StyleSheet.create({ + container: { flex: 1, paddingVertical: 5, paddingHorizontal: 5 }, + secContainer: { + flex: 1, + flexDirection: "row", + flexWrap: "wrap", + }, + earthIcon: { height: 15, width: 15 }, + timeSlotContainer: { + height: 32, + width: (300 - 40) / 3, + marginVertical: 5, + marginHorizontal: 4, + justifyContent: "center", + alignItems: "center", + borderRadius: 5, + }, +}); diff --git a/src/shared/views/index.ts b/src/shared/views/index.ts index 4876b90..bbdacb9 100644 --- a/src/shared/views/index.ts +++ b/src/shared/views/index.ts @@ -86,6 +86,7 @@ import { } from './CometChatMediaRecorder'; import { CometChatFormBubble, CometChatFormBubbleInterface } from './CometChatFormBubble' import { CometChatCardBubble, CometChatCardBubbleInterface } from './CometChatCardBubble' +import { CometChatSchedulerBubble, CometChatSchedulerBubbleInterface } from './CometChatSchedulerBubble' export { CometChatMessageInputStyleInterface, @@ -165,5 +166,7 @@ export { CometChatFormBubble, CometChatFormBubbleInterface, CometChatCardBubble, - CometChatCardBubbleInterface + CometChatCardBubbleInterface, + CometChatSchedulerBubble, + CometChatSchedulerBubbleInterface };