From e0ffce1cade156b10ddafea692fec440e07908fd Mon Sep 17 00:00:00 2001 From: root Date: Sun, 3 Jan 2016 09:51:27 +0000 Subject: [PATCH 001/140] Add locale for Vietnamese --- config/locales/client.vi_VN.yml | 2082 +++++++++++++++++ config/locales/server.vi_VN.yml | 1021 ++++++++ plugins/poll/config/locales/client.vi_VN.yml | 31 + plugins/poll/config/locales/server.vi_VN.yml | 25 + public/403.vi_VN.html | 27 + public/422.vi_VN.html | 26 + public/500.vi_VN.html | 13 + public/503.vi_VN.html | 12 + .../discourse_imgur/locale/server.vi_VN.yml | 6 + 9 files changed, 3243 insertions(+) create mode 100644 config/locales/client.vi_VN.yml create mode 100644 config/locales/server.vi_VN.yml create mode 100644 plugins/poll/config/locales/client.vi_VN.yml create mode 100644 plugins/poll/config/locales/server.vi_VN.yml create mode 100644 public/403.vi_VN.html create mode 100644 public/422.vi_VN.html create mode 100644 public/500.vi_VN.html create mode 100644 public/503.vi_VN.html create mode 100644 vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi_VN.yml diff --git a/config/locales/client.vi_VN.yml b/config/locales/client.vi_VN.yml new file mode 100644 index 0000000000..0ff10f7685 --- /dev/null +++ b/config/locales/client.vi_VN.yml @@ -0,0 +1,2082 @@ +vi_VN: + js: + number: + format: + separator: "." + delimiter: "," + human: + storage_units: + format: '%n %u' + units: + byte: + other: Byte + gb: GB + kb: KB + mb: MB + tb: TB + short: + thousands: "{{number}}k" + millions: "{{number}}M" + dates: + time: "h:mm a" + long_no_year: "MMM D h:mm a" + long_no_year_no_time: "MMM D" + full_no_year_no_time: "MMMM Do" + long_with_year: "MMM D, YYYY h:mm a" + long_with_year_no_time: "MMM D, YYYY" + full_with_year_no_time: "MMMM Do, YYYY" + long_date_with_year: "MMM D, 'YY LT" + long_date_without_year: "MMM D, LT" + long_date_with_year_without_time: "MMM D, 'YY" + long_date_without_year_with_linebreak: "MMM D
LT" + long_date_with_year_with_linebreak: "MMM D, 'YY
LT" + tiny: + half_a_minute: "< 1m" + less_than_x_seconds: + other: "< %{count}s" + x_seconds: + other: "%{count}s" + less_than_x_minutes: + other: "< %{count}m" + x_minutes: + other: "%{count}m" + about_x_hours: + other: "%{count}h" + x_days: + other: "%{count}d" + about_x_years: + other: "%{count}y" + over_x_years: + other: "> %{count}y" + almost_x_years: + other: "%{count}y" + date_month: "MMM D" + date_year: "MMM 'YY" + medium: + x_minutes: + other: "%{count} phút" + x_hours: + other: "%{count} giờ" + x_days: + other: "%{count} ngày" + date_year: "MMM D, 'YY" + medium_with_ago: + x_minutes: + other: " %{count} phút trước" + x_hours: + other: "%{count} giờ trước" + x_days: + other: "%{count} ngày trước" + later: + x_days: + other: "còn %{count} ngày" + x_months: + other: "còn %{count} tháng" + x_years: + other: "còn %{count} năm" + share: + topic: 'chia sẽ chủ đề này' + post: 'đăng #%{Bài đăng số}' + close: 'đóng lại' + twitter: 'chia sẽ lên Twitter' + facebook: 'chia sẽ lên Facebook' + google+: 'chia sẽ lên Google+' + email: 'Gửi liên kết này qua thư điện tử' + action_codes: + split_topic: "chìa chủ đề này lúc %{when}" + autoclosed: + enabled: 'đóng lúc %{when}' + disabled: 'mở lúc %{when}' + closed: + enabled: 'đóng lúc %{when}' + disabled: 'mở lúc %{when}' + archived: + enabled: 'lưu trữ %{when}' + disabled: 'bỏ lưu trữ %{when}' + pinned: + enabled: 'gắn lúc %{when}' + disabled: 'bỏ gim %{when}' + pinned_globally: + enabled: 'đã gắn toàn trang %{when}' + disabled: 'đã bỏ gắn %{when}' + visible: + enabled: 'đã lưu %{when}' + disabled: 'bỏ lưu %{when}' + topic_admin_menu: "quản lí chủ đề." + emails_are_disabled: "Ban quản trị đã chặn mọi email đang gửi. Sẽ không có bắt kỳ thông báo nào về email được gửi đi." + edit: 'thay đổi tiêu đề và chuyên mục của chủ đề' + not_implemented: "Tính năng này chưa được hoàn thiện hết, xin lỗi!" + no_value: "Không" + yes_value: "Có" + generic_error: "Rất tiếc, đã có lỗi xảy ra." + generic_error_with_reason: "Đã xảy ra lỗi: %{error}" + sign_up: "Đăng ký" + log_in: "Đăng nhập" + age: "Độ tuổi" + joined: "Đã tham gia" + admin_title: "Quản trị" + flags_title: "Báo cáo" + show_more: "hiện thêm" + show_help: "lựa chọn" + links: "Liên kết" + links_lowercase: + other: "liên kết" + faq: "FAQ" + guidelines: "Hướng dẫn" + privacy_policy: "Chính sách riêng tư" + privacy: "Sự riêng tư" + terms_of_service: "Điều khoản dịch vụ" + mobile_view: "Xem ở chế độ di động" + desktop_view: "Xem ở chế độ máy tính" + you: "Bạn" + or: "hoặc" + now: "ngay lúc này" + read_more: 'đọc thêm' + more: "Nhiều hơn" + less: "Ít hơn" + never: "không bao giờ" + daily: "hàng ngày" + weekly: "hàng tuần" + every_two_weeks: "mỗi hai tuần" + every_three_days: "ba ngày một" + max_of_count: "tối đa của {{count}}" + alternation: "hoặc" + character_count: + other: "{{count}} ký tự" + suggested_topics: + title: "Chủ đề tương tự" + about: + simple_title: "Giới thiệu" + title: "Giới thiệu về %{title}" + stats: "Thống kê của trang" + our_admins: "Các quản trị viên" + our_moderators: "Các điều hành viên" + stat: + all_time: "Từ trước tới nay" + last_7_days: "7 ngày qua" + last_30_days: "30 ngày vừa qua" + like_count: "Lượt thích" + topic_count: "Các chủ đề" + post_count: "Các bài viết" + user_count: "Thành viên mới" + active_user_count: "Thành viên tích cực" + contact: "Contact Us" + contact_info: "Trong trường hợp có bất kỳ sự cố nào ảnh hưởng tới trang này, xin vui lòng liên hệ với chúng tôi theo địa chỉ %{contact_info}." + bookmarked: + title: "Bookmark" + clear_bookmarks: "Clear Bookmarks" + help: + bookmark: "Chọn bài viết đầu tiên của chủ đề cho vào bookmark" + unbookmark: "Chọn để xoá toàn bộ bookmark trong chủ đề này" + bookmarks: + not_logged_in: "rất tiếc, bạn phải đăng nhập để có thể đánh dấu bài viết" + created: "bạn đã đánh dấu bài viết này" + not_bookmarked: "bạn đã đọc bài viết này; nhấp chuột để đánh dấu" + last_read: "đây là bài viết cuối cùng bạn đã đọc; nhấp chuột để đánh dấu" + remove: "Xóa đánh dấu" + confirm_clear: "Bạn có chắc muốn xóa tất cả đánh dấu ở topic này?" + topic_count_latest: + other: "{{count}} chủ đề mới hoặc đã cập nhật." + topic_count_unread: + other: "{{count}} chủ đề chưa đọc." + topic_count_new: + other: "{{count}} chủ đề mới." + click_to_show: "Nhấp chuột để hiển thị." + preview: "xem trước" + cancel: "hủy" + save: "Lưu thay đổi" + saving: "Đang lưu ..." + saved: "Đã lưu!" + upload: "Tải lên" + uploading: "Đang tải lên..." + uploading_filename: "Đang tải lên {{filename}}..." + uploaded: "Đã tải lên!" + enable: "Kích hoạt" + disable: "Vô hiệu hóa" + undo: "Hoàn tác" + revert: "Phục hồi" + failed: "Thất bại" + switch_to_anon: "Chế độ Ẩn danh" + switch_from_anon: "Thoát ẩn danh" + banner: + close: "Xóa biểu ngữ này." + edit: "Sửa banner này >>" + choose_topic: + none_found: "Không tìm thấy chủ đề nào" + title: + search: "Tìm kiếm chủ đề dựa vào tên, url hoặc id:" + placeholder: "viết tiêu đề của chủ đề thảo luận ở đây" + queue: + topic: "Chủ đề" + approve: 'Phê duyệt' + reject: 'Từ chối' + delete_user: 'Xóa tài khoản' + title: "Cần phê duyệt" + none: "Không có bài viết nào để xem trước" + edit: "Sửa" + cancel: "Hủy" + view_pending: "xem bài viết đang chờ xử lý" + has_pending_posts: + other: "Chủ đề này có {{count}} bài viết cần phê chuẩn" + confirm: "Lưu thay đổi" + delete_prompt: "Bạn có chắc muốn xóa %{username}? Tất cả các bài viết của họ sẽ bị xóa, email và ip sẽ bị chặn." + approval: + title: "Bài viết cần phê duyệt" + description: "Chúng tôi đã nhận được bài viết mới của bạn, nhưng nó cần phải được phê duyệt bởi admin trước khi được hiện. Xin hãy kiên nhẫn." + pending_posts: + other: "Bạn có {{count}} bài viết đang chờ xử lý." + ok: "OK" + user_action: + user_posted_topic: "{{user}} đăng chủ đề" + you_posted_topic: "Bạn đã đăng chủ đề" + user_replied_to_post: "{{user}} đã trả lời tới {{post_number}}" + you_replied_to_post: "Bạn đã trả lời tới {{post_number}}" + user_replied_to_topic: "{{user}} đã trả lời chủ đề" + you_replied_to_topic: "Bạn đã trả lời tới chủ đề" + user_mentioned_user: "{{user}} đã nhắc đến {{another_user}}" + user_mentioned_you: "{{user}} đã nhắc tới bạn" + you_mentioned_user: "Bạn đã đề cập {{another_user}}" + posted_by_user: "Được đăng bởi {{user}}" + posted_by_you: "Được đăng bởi bạn" + sent_by_user: "Đã gửi bởi {{user}}" + sent_by_you: "Đã gửi bởi bạn" + directory: + filter_name: "lọc theo tên đăng nhập" + title: "Thành viên" + likes_given: "Đưa ra" + likes_received: "Đã nhận" + topics_entered: "Được nhập" + topics_entered_long: "Chủ đề được nhập" + time_read: "Thời gian đọc" + topic_count: "Chủ đề" + topic_count_long: "Chủ đề đã được tạo" + post_count: "Trả lời" + post_count_long: "Trả lời đã được đăng" + no_results: "Không tìm thấy kết quả." + days_visited: "Ghé thăm" + days_visited_long: "Ngày đã ghé thăm" + posts_read: "Đọc" + posts_read_long: "Đọc bài viết" + total_rows: + other: "%{count} người dùng" + groups: + visible: "Mọi thành viên có thể nhìn thấy nhóm" + title: + other: "các nhóm" + members: "Các thành viên" + posts: "Các bài viết" + alias_levels: + nobody: "Không ai cả" + only_admins: "Chỉ các quản trị viên" + mods_and_admins: "Chỉ có người điều hành và ban quản trị" + members_mods_and_admins: "Chỉ có thành viên trong nhóm, ban điều hành, và ban quản trị" + everyone: "Mọi người" + trust_levels: + title: "Cấp độ tin tưởng tự động tăng cho thành viên khi họ thêm:" + none: "Không có gì" + user_action_groups: + '1': "Lần thích" + '2': "Lần được thích" + '3': "Chỉ mục" + '4': "Các chủ đề" + '5': "Trả lời" + '6': "Phản hồi" + '7': "Được nhắc đến" + '9': "Lời trích dẫn" + '11': "Biên tập" + '12': "Bài đã gửi" + '13': "Hộp thư" + '14': "Đang chờ xử lý" + categories: + all: "tất cả chuyên mục" + all_subcategories: "Tất cả" + no_subcategory: "không có gì" + category: "Chuyên mục" + reorder: + title: "Sắp xếp lại danh mục" + title_long: "Tổ chức lại danh sách danh mục" + fix_order: "Vị trí cố định" + fix_order_tooltip: "Không phải tất cả danh mục có vị trí duy nhất, điều này có thể dẫn đến kết quả không mong muốn." + save: "Lưu thứ tự" + apply_all: "Áp dụng" + position: "Vị trí" + posts: "Bài viết" + topics: "Chủ đề" + latest: "Mới nhất" + latest_by: "mới nhất bởi" + toggle_ordering: "chuyển lệnh kiểm soát" + subcategories: "Phân loại phụ" + topic_stats: "Số lượng chủ đề mới" + topic_stat_sentence: + other: "%{count} số lượng chủ đề mới tỏng quá khứ %{unit}." + post_stats: "Số lượng bài viết mới" + post_stat_sentence: + other: "%{count} số lượng bài viết mới trong quá khứ %{unit}." + ip_lookup: + title: Tìm kiếm địa chỉ IP + hostname: Hostname + location: Vị trí + location_not_found: (không biết) + organisation: Công ty + phone: Điện thoại + other_accounts: "Tài khoản khác với địa chỉ IP này" + delete_other_accounts: "Xoá %{count}" + username: "tên đăng nhập" + trust_level: "TL" + read_time: "thời gian đọc" + topics_entered: "chủ để đã xem" + post_count: "# bài viết" + confirm_delete_other_accounts: "Bạn có muốn xóa những tài khoản này không?" + user_fields: + none: "(chọn một tùy chọn)" + user: + said: "{{username}}:" + profile: "Tiểu sử" + mute: "Im lặng" + edit: "Tùy chỉnh" + download_archive: "Tải bài viết về" + new_private_message: "Tin nhắn mới" + private_message: "Tin nhắn" + private_messages: "Các tin nhắn" + activity_stream: "Hoạt động" + preferences: "Tùy chỉnh" + expand_profile: "Mở" + bookmarks: "Theo dõi" + bio: "Về tôi" + invited_by: "Được mời bởi" + trust_level: "Độ tin tưởng" + notifications: "Thông báo" + desktop_notifications: + label: "Desktop Notifications" + not_supported: "Xin lỗi. Trình duyệt của bạn không hỗ trợ Notification." + perm_default: "Mở thông báo" + perm_denied_btn: "Không có quyền" + perm_denied_expl: "Bạn bị từ chối quyền cho notification. Dùng trình duyệt của bạn để kích hoạt notification, sau đó nhấp nút này khi hoàn thành. (Desktop: Icon bên trái của thanh địa chỉ. Mobile: 'Site Info'.)" + disable: "Khóa Notification" + currently_enabled: "(đang cho phép)" + enable: "Cho phép Notification" + currently_disabled: "(hiện tại không cho phép)" + each_browser_note: "Lưu ý: Bạn phải thay đổi trong cấu hình mỗi trình duyệt bạn sử dụng." + dismiss_notifications: "Đánh dấu đã đọc cho tất cả" + dismiss_notifications_tooltip: "Đánh dấu đã đọc cho tất cả các thông báo chưa đọc" + disable_jump_reply: "Đừng tới bài viết của tôi sau khi tôi trả lời" + dynamic_favicon: "Hiện số chủ đề mới / cập nhật vào biểu tượng trình duyệt" + edit_history_public: "Để thành viên khác xem những sửa chữa bài viết của bạn" + external_links_in_new_tab: "Mở tất cả liên kết bên ngoài trong thẻ mới" + enable_quoting: "Bật chế độ làm nổi bật chữ trong đoạn trích dẫn trả lời" + change: "thay đổi" + moderator: "{{user}} trong ban quản trị" + admin: "{{user}} là người điều hành" + moderator_tooltip: "Thành viên này là MOD" + admin_tooltip: "Thành viên này là admin" + blocked_tooltip: "Tài khoản này bị khóa" + suspended_notice: "Thành viên này bị đình chỉ cho đến ngày {{date}}. " + suspended_reason: "Lý do: " + github_profile: "Github" + mailing_list_mode: "Gửi email cho tôi mỗi bài viết mới (trừ khi tôi tắt chủ đề hoặc chuyên mục)" + watched_categories: "Xem" + watched_categories_instructions: "Bạn sẽ tự động xem tất cả các chủ đề mới trong các chuyên mục này. Bạn sẽ được thông báo về tất các các bài viết mới, và một số các bài viết mới cũng sẽ xuất hiện ở chủ đề kế tiếp." + tracked_categories: "Theo dõi" + tracked_categories_instructions: "Bạn sẽ tự động theo dõi tất cả các chủ đề trong các chuyên mục này. Một số bài viết mới sẽ xuất hiện ở chủ đề kế tiếp." + muted_categories: "Im lặng" + delete_account: "Xoá Tài khoản của tôi" + delete_account_confirm: "Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của bạn? Hành động này không thể được hoàn tác!" + deleted_yourself: "Tài khoản của bạn đã được xóa thành công." + delete_yourself_not_allowed: "Bạn không thể xóa tài khoản của bạn ngay bây giờ. Liên lạc với admin để làm xóa tài khoản cho bạn." + unread_message_count: "Tin nhắn" + admin_delete: "Xoá" + users: "Thành viên" + muted_users: "Im lặng" + muted_users_instructions: "Ngăn chặn tất cả các thông báo từ những thành viên." + staff_counters: + flags_given: "cờ hữu ích" + flagged_posts: "bài viết gắn cờ" + deleted_posts: "bài viết bị xoá" + suspensions: "đình chỉ" + warnings_received: "cảnh báo" + messages: + all: "Tất cả" + change_password: + success: "(email đã gửi)" + in_progress: "(đang gửi email)" + error: "(lỗi)" + action: "Gửi lại mật khẩu tới email" + set_password: "Nhập Mật khẩu" + change_about: + title: "Thay đổi thông tin về tôi" + error: "Có lỗi khi thay đổi giá trị này." + change_username: + title: "Thay Username" + confirm: "Nếu bạn thay đổi tên đăng nhập, tất cả những câu nói của bạn ở bài viết trước và @tên bạn sẽ được đề cập sẽ bị phá vỡ. Bạn có chắc bạn muốn tiếp tục không?" + taken: "Xin lỗi, đã có username này." + error: "Có lỗi trong khi thay đổi username của bạn." + invalid: "Username này không thích hợp. Nó chỉ chứa các ký tự là chữ cái và chữ số. " + change_email: + title: "Thay đổi Email" + taken: "Xin lỗi, email này không dùng được. " + error: "Có lỗi xảy ra khi thay đổi email của bạn. Có thể địa chỉ email đã được sử dụng ?" + success: "Chúng tôi đã gửi email tới địa chỉ đó. Vui lòng làm theo chỉ dẫn để xác nhận lại." + change_avatar: + title: "Đổi ảnh đại diện" + gravatar: "dựa trên Gravatar" + gravatar_title: "Đổi ảnh đại diện của bạn trên website Gravatar" + refresh_gravatar_title: "Làm mới Gravatar của bạn" + letter_based: "Hệ thống xác định ảnh đại diện" + uploaded_avatar: "Chính sửa hình ảnh" + uploaded_avatar_empty: "Thêm một ảnh chỉnh sửa" + upload_title: "Upload hình ảnh của bạn" + upload_picture: "Úp hình" + image_is_not_a_square: "Cảnh báo: chúng tôi đã cắt hình ảnh của bạn; chiều rộng và chiều cao không bằng nhau." + cache_notice: "Hình hồ sở của bạn đã thay đổi thành công nhưng có thể thỉnh thoảng xuất hiện ảnh cũ bởi cache của trình duyệt." + change_profile_background: + title: "Hình nền trang hồ sơ" + instructions: "Hình nền trang hồ sơ sẽ ở giữa và có chiều rộng mặc định là 850px." + change_card_background: + title: "Hình nền Card" + instructions: "Hình nền sẽ ở giữa và có chiều rộng mặc định là 590px." + email: + title: "Email" + instructions: "Không bao giờ công khai" + ok: "Chúng tôi sẽ gửi thư điện tử xác nhận đến cho bạn" + invalid: "Vùi lòng nhập một thư điện tử hợp lệ" + authenticated: "Thư điện tử của bạn đã được xác nhận bởi {{provider}}" + name: + title: "Tên" + instructions: "Tên đầy đủ của bạn (tùy chọn)" + instructions_required: "Tên đầy đủ của bạn" + too_short: "Tên của bạn quá ngắn" + ok: "Tên của bạn có vẻ ổn" + username: + title: "Username" + instructions: "Duy nhất, không khoảng trắng" + short_instructions: "Mọi người có thể nhắc tới bạn bằng @{{username}}" + available: "Tên đăng nhập của bạn có sẵn" + global_match: "Email đúng với username đã được đăng ký" + global_mismatch: "Đã đăng ký rồi. Thử {{suggestion}}?" + not_available: "Chưa có sẵn. Thử {{suggestion}}?" + too_short: "Tên đăng nhập của bạn quá ngắn" + too_long: "Tên đăng nhập của bạn quá dài" + checking: "Đang kiểm tra username sẵn sàng để sử dụng...." + enter_email: 'Đã tìm được tên đăng nhập. Điền thư điện tử phù hợp.' + prefilled: "Thư điện tử trủng với tên đăng nhập này." + locale: + title: "Ngôn ngữ hiển thị" + instructions: "Ngôn ngữ hiển thị sẽ thay đổi khi bạn tải lại trang" + default: "(mặc định)" + password_confirmation: + title: "Nhập lại Password" + last_posted: "Bài viết cuối cùng" + last_emailed: "Đã email lần cuối" + last_seen: "được thấy" + created: "Đã tham gia" + log_out: "Log Out" + location: "Vị trí" + card_badge: + title: "Huy hiệu của thẻ thành viên" + website: "Web Site" + email_settings: "Email" + email_digests: + title: "Khi tôi không truy cập, gửi email gợi ý những cái mới cho tôi:" + daily: "hàng ngày" + every_three_days: "ba ngày một" + weekly: "hàng tuần" + every_two_weeks: "hai tuần một" + email_direct: "Gửi cho tôi một email khi có người trích dẫn, trả lời cho bài viết của tôi, đề cập đến @username của tôi, hoặc mời tôi đến một chủ đề" + email_private_messages: "Gửi cho tôi email khi có ai đó nhắn tin cho tôi" + email_always: "Gửi email thông báo cho tôi mỗi khi tôi kích hoạt trên website này" + other_settings: "Khác" + categories_settings: "Chuyên mục" + new_topic_duration: + label: "Để ý tới chủ đề mới khi" + not_viewed: "Tôi chưa từng xem họ" + last_here: "tạo ra kể từ lần cuối tôi ở đây" + after_1_day: "được tạo ngày hôm qua" + after_2_days: "được tạo 2 ngày trước" + after_1_week: "được tạo tuần trước" + after_2_weeks: "được tạo 2 tuần trước" + auto_track_topics: "Tự động theo dõi các chủ đề tôi tạo" + auto_track_options: + never: "không bao giờ" + immediately: "ngay lập tức" + after_30_seconds: "sau 30 giây" + after_1_minute: "sau 1 phút" + after_2_minutes: "sau 2 phút" + after_3_minutes: "sau 3 phút" + after_4_minutes: "sau 4 phút" + after_5_minutes: "sau 5 phút" + after_10_minutes: "sau 10 phút" + invited: + search: "gõ để tìm kiếm thư mời " + title: "Lời mời" + user: "User được mời" + sent: "Đã gửi" + redeemed: "Lời mời bù lại" + redeemed_tab: "Làm lại" + redeemed_tab_with_count: "Làm lại ({{count}})" + redeemed_at: "Nhận giải" + pending: "Lời mời tạm hoãn" + pending_tab: "Đang treo" + pending_tab_with_count: "Đang xử lý ({{count}})" + topics_entered: "Bài viết được xem " + posts_read_count: "Đọc bài viết" + expired: "Thư mời này đã hết hạn." + rescind: "Xoá" + rescinded: "Lời mời bị xóa" + reinvite: "Mời lại" + reinvited: "Gửi lại lời mời" + time_read: "Đọc thời gian" + days_visited: "Số ngày đã thăm" + account_age_days: "Thời gian của tài khoản theo ngày" + create: "Gửi một lời mời" + generate_link: "Chép liên kết Mời" + bulk_invite: + none: "Bạn đã mời ai ở đây chưa. Bạn có thể mời một hoặc một nhóm bằng tải lên hàng loạt file mời." + text: "Mời hàng loạt bằng file" + uploading: "Uploading..." + success: "Tải lên thành công, bạn sẽ được thông báo qua tin nhắn khi quá trình hoàn tất." + error: "Có lỗi xảy ra khi upload '{{filename}}': {{message}}" + password: + title: "Mật khẩu" + too_short: "Mật khẩu của bạn quá ngắn." + common: "Mật khẩu quá đơn giản, rất dễ bị đoán ra" + same_as_username: "Mật khẩu của bạn trùng với tên đăng nhập." + same_as_email: "Mật khẩu của bạn trùng với email của bạn." + ok: "Mật khẩu của bạn có vẻ ổn." + instructions: "Ít nhất %{count} ký tự" + associated_accounts: "Đăng nhập" + ip_address: + title: "Địa chỉ IP cuối cùng" + registration_ip_address: + title: "Địa chỉ IP đăng ký" + avatar: + title: "Ảnh đại diện" + header_title: "hồ sơ cá nhân, tin nhắn, đánh dấu và sở thích" + title: + title: "Tiêu đề" + filters: + all: "All" + stream: + posted_by: "Đăng bởi" + sent_by: "Gửi bởi" + private_message: "tin nhắn" + the_topic: "chủ đề" + loading: "Đang tải..." + errors: + prev_page: "trong khi cố gắng để tải" + reasons: + network: "Mạng Internet bị lỗi" + server: "Máy chủ đang có vấn đề" + forbidden: "Bạn không thể xem được" + unknown: "Lỗi" + not_found: "Không Tìm Thấy Trang" + desc: + network: "Hãy kiểm tra kết nối của bạn" + network_fixed: "Hình như nó trở lại." + server: "Mã lỗi : {{status}}" + forbidden: "Bạn không được cho phép để xem mục này" + unknown: "Có một lỗi gì đó đang xảy ra" + buttons: + back: "Quay trở lại" + again: "Thử lại" + fixed: "Load lại trang" + close: "Đóng lại" + assets_changed_confirm: "Website đã được cập nhật bản mới. Bạn có thể làm mới lại trang để có thể sử dụng bản mới được cập nhật" + logout: "Bạn đã đăng xuất" + refresh: "Tải lại" + read_only_mode: + enabled: "Chế độ chỉ đọc được kích hoạt. Bạn có thể tiếp tục duyệt tới trang web, nhưng các tương tác có thể không hoạt động." + login_disabled: "Chức năng Đăng nhập đã bị tắt khi website trong trạng thái chỉ đọc" + learn_more: "tìm hiểu thêm..." + year: 'năm' + year_desc: 'chủ đề được tạo ra trong 365 ngày qua' + month: 'tháng' + month_desc: 'chủ đề được tạo ra trong 30 ngày qua' + week: 'tuần' + week_desc: 'chủ đề được tạo ra trong 7 ngày qua' + day: 'ngày' + first_post: Bài viết đầu tiên + mute: Im lặng + unmute: Bỏ im lặng + last_post: Bài viết cuối cùng + last_reply_lowercase: trả lời cuối cùng + replies_lowercase: + other: trả lời + signup_cta: + sign_up: "Đăng ký" + hide_session: "Nhắc vào ngày mai" + hide_forever: "không, cảm ơn" + summary: + enabled_description: "Bạn đang xem một bản tóm tắt của chủ đề này: các bài viết thú vị nhất được xác định bởi cộng đồng." + description: "Có {{count}} trả lời" + description_time: "Đây là {{count}} trả lời tương ứng với thời gian đọc {{readingTime}} phút." + enable: 'Tóm tắt lại chủ đề' + disable: 'HIển thị tất cả các bài viết' + deleted_filter: + enabled_description: "Chủ để này có chứa các bài viết bị xoá, chúng đã bị ẩn đi" + disabled_description: "Xoá các bài viết trong các chủ để được hiển thị" + enable: "Ẩn các bài viết bị xoá" + disable: "Xem các bài viết bị xoá" + private_message_info: + title: "Tin nhắn" + invite: "Mời người khác..." + remove_allowed_user: "Bạn thực sự muốn xóa {{name}} từ tin nhắn này?" + email: 'Email' + username: 'Username' + last_seen: 'Đã xem' + created: 'Tạo bởi' + created_lowercase: 'ngày tạo' + trust_level: 'Độ tin tưởng' + search_hint: 'username, email or IP address' + create_account: + title: "Tạo tài khoản mới" + failed: "Có gì đó không đúng, có thể email này đã được đăng ký, thử liên kết quên mật khẩu" + forgot_password: + title: "Đặt lại mật khẩu" + action: "Tôi đã quên mật khẩu của tôi" + invite: "Điền vào username của bạn hoặc địa chỉ email và chúng tôi sẽ gửi bạn email để khởi tạo lại mật khẩu" + reset: "Tạo lại mật khẩu" + complete_username: "Nếu một tài khoản phù hợp với tên thành viên % {username} , bạn sẽ nhận được một email với hướng dẫn về cách đặt lại mật khẩu của bạn trong thời gian ngắn." + complete_email: "Nếu một trận đấu tài khoản % {email} , bạn sẽ nhận được một email với hướng dẫn về cách đặt lại mật khẩu của bạn trong thời gian ngắn." + complete_username_found: "Chúng tôi tìm thấy một tài khoản phù hợp với tên thành viên % {username} , bạn sẽ nhận được một email với hướng dẫn về cách đặt lại mật khẩu của bạn trong thời gian ngắn." + complete_email_found: "Chúng tôi tìm thấy một tài khoản phù hợp với % {email} , bạn sẽ nhận được một email với hướng dẫn về cách đặt lại mật khẩu của bạn trong thời gian ngắn." + complete_username_not_found: "Không có tài khoản phù hợp với tên thành viên % {username} " + complete_email_not_found: "Không tìm thấy tài khoản nào tương ứng với %{email}" + login: + title: "Đăng nhập" + username: "Thành viên" + password: "Mật khẩu" + email_placeholder: "Email hoặc tên đăng nhập " + caps_lock_warning: "Phím Caps Lock đang được bật" + error: "Không xác định được lỗi" + rate_limit: "Xin đợi trước khi đăng nhập lại lần nữa." + blank_username_or_password: "Bạn phải nhập email hoặc username, và mật khẩu" + reset_password: 'Khởi tạo mật khẩu' + logging_in: "Đăng nhập..." + or: "Hoặc" + authenticating: "Đang xác thực..." + awaiting_confirmation: "Tài khoản của bạn đang đợi kích hoạt, sử dụng liên kết quên mật khẩu trong trường hợp kích hoạt ở 1 email khác." + awaiting_approval: "Tài khoản của bạn chưa được chấp nhận bới thành viên. Bạn sẽ được gửi một email khi được chấp thuận " + requires_invite: "Xin lỗi, bạn phải được mời để tham gia diễn đàn" + not_activated: "Bạn không thể đăng nhập. Chúng tôi đã gửi trước email kích hoạt cho bạn tại {{sentTo}}. Vui lòng làm theo hướng dẫn trong email để kích hoạt tài khoản của bạn." + not_allowed_from_ip_address: "Bạn không thể đăng nhập từ địa chỉ IP này" + admin_not_allowed_from_ip_address: "Bạn không thể đăng nhập với quyền quản trị từ địa chỉ IP đó." + resend_activation_email: "Bấm đây để gửi lại email kích hoạt" + sent_activation_email_again: "Chúng tôi gửi email kích hoạt tới cho bạn ở {{currentEmail}}. Nó sẽ mất vài phút để đến; bạn nhớ check cả hồm thư spam nhe. " + to_continue: "Vui lòng đăng nhập" + google: + title: "với Google " + message: "Chứng thực với Google (Bạn hãy chắc chắn là chặn popup không bật)" + google_oauth2: + title: "với Google" + message: "Chứng thực với Google (chắc chắn rằng cửa sổ pop up blocker không được kích hoạt)" + twitter: + title: "với Twitter" + message: "Chứng thực với Twitter(hãy chắc chắn là chăn pop up không bật)" + facebook: + title: "với Facebook" + message: "Chứng thực với Facebook(chắc chắn là chặn pop up không bật)" + yahoo: + title: "với Yahoo" + message: "Chứng thực với Yahoo (Chắc chắn chặn pop up không bật)" + github: + title: "với GitHub" + message: "Chứng thực với GitHub (chắc chắn chặn popup không bật)" + apple_international: "Apple/International" + google: "Google" + twitter: "Twitter" + emoji_one: "Emoji One" + composer: + emoji: "Emoji :smile:" + options: "Lựa chọn" + whisper: "nói chuyện" + add_warning: "Đây là một cảnh báo chính thức" + toggle_whisper: "Chuyển chế độ Nói chuyện" + posting_not_on_topic: "Bài viết nào bạn muốn trả lời " + saving_draft_tip: "đang lưu..." + saved_draft_tip: "Đã lưu" + saved_local_draft_tip: "Đã lưu locally" + similar_topics: "Bài viết của bạn tương tự với " + drafts_offline: "Nháp offline" + error: + title_missing: "Tiêu đề là bắt buộc" + title_too_short: "Tiêu để phải có ít nhất {{min}} ký tự" + title_too_long: "Tiêu đề có tối đa {{max}} ký tự" + post_missing: "Bài viết không được bỏ trắng" + post_length: "Bài viết phải có ít nhất {{min}} ký tự" + try_like: 'Các bạn đã thử các nút ?' + category_missing: "Bạn phải chọn một phân loại" + save_edit: "Lưu chỉnh sửa" + reply_original: "Trả lời cho bài viết gốc" + reply_here: "Trả lời đây " + reply: "Trả lời " + cancel: "Huỷ" + create_topic: "Tạo chủ đề" + create_pm: "Tin nhắn" + title: "Hoặc nhất Ctrl+Enter" + users_placeholder: "Thêm thành viên " + title_placeholder: "Tóm tắt lại thảo luận này trong một câu ngắn gọn" + edit_reason_placeholder: "Tại sao bạn sửa" + show_edit_reason: "(thêm lý do sửa)" + reply_placeholder: "Gõ ở đây. Sử dụng Markdown, BBCode, hoặc HTML để định dạng. Kéo hoặc dán ảnh." + view_new_post: "Xem bài đăng mới của bạn. " + saved: "Đã lưu" + saved_draft: "Bài nháp đang lưu. Chọn để tiếp tục." + uploading: "Đang đăng " + show_preview: 'Xem trước »' + hide_preview: '«ẩn xem trước' + quote_post_title: "Trích dẫn cả bài viết" + bold_title: "In đậm" + bold_text: "chữ in đậm" + italic_title: "Nhấn mạnh" + italic_text: "văn bản nhấn mạnh" + link_title: "Liên kết" + link_description: "Nhập mô tả liên kết ở đây" + link_dialog_title: "Chèn liên kết" + link_optional_text: "tiêu đề tùy chọn" + quote_title: "Trích dẫn" + quote_text: "Trích dẫn" + code_title: "Văn bản định dạng trước" + code_text: "lùi đầu dòng bằng 4 dấu cách" + upload_title: "Tải lên" + upload_description: "Nhập mô tả tải lên ở đây" + olist_title: "Danh sách kiểu số" + ulist_title: "Danh sách kiểu ký hiệu" + list_item: "Danh sách các mục" + heading_title: "Tiêu đề" + heading_text: "Tiêu đề" + hr_title: "Căn ngang" + help: "Trợ giúp soạn thảo bằng Markdown" + toggler: "ẩn hoặc hiển thị bảng điều khiển soạn thảo" + modal_cancel: "Hủy" + admin_options_title: "Tùy chọn quản trị viên cho chủ đề này" + auto_close: + label: "Thời gian tự khóa chủ đề:" + error: "Vui lòng nhập một giá trị hợp lệ." + based_on_last_post: "Không đóng cho đến khi bài viết cuối cùng trong chủ đề này trở thành bài cũ" + all: + examples: 'Nhập giờ (định dạng 24h), thời gian chính xác ( vd: 17:30) hoặc thời gian kèm ngày tháng (2013-11-22 14:00).' + limited: + units: "(# của giờ)" + examples: 'Nhập số giờ ( theo định dạng 24h)' + notifications: + title: "thông báo của @name nhắc đến, trả lời bài của bạn và chủ đề, tin nhắn, vv" + none: "Không thể tải các thông báo tại thời điểm này." + more: "xem thông báo cũ hơn" + total_flagged: "tổng số bài viết gắn cờ" + mentioned: "

{{username}} {{description}}

" + quoted: "

{{username}} {{description}}

" + replied: "

{{username}} {{description}}

" + posted: "

{{username}} {{description}}

" + edited: "

{{username}} {{description}}

" + liked: "

{{username}} {{description}}

" + private_message: "

{{username}} {{description}}

" + invited_to_private_message: "

{{username}} {{description}}

" + invited_to_topic: "

{{username}} {{description}}

" + invitee_accepted: "

{{username}} chấp nhận lời mời của bạn

" + moved_post: "

{{username}} chuyển {{description}}

" + linked: "

{{username}} {{description}}

" + granted_badge: "

Thu được '{{description}}'

" + alt: + mentioned: "Được nhắc đến bởi" + quoted: "Trích dẫn bởi" + replied: "Đã trả lời" + posted: "Đăng bởi" + edited: "Bài viết của bạn được sửa bởi" + liked: "Bạn đã like bài viết" + private_message: "Tin nhắn riêng từ" + invitee_accepted: "Lời mời được chấp nhận bởi" + moved_post: "Bài viết của bạn đã được di chuyển bởi" + linked: "Liên kết đến bài viết của bạn" + popup: + mentioned: '{{username}} nhắc đến bạn trong "{{topic}}" - {{site_title}}' + quoted: '{{username}} trích lời bạn trong "{{topic}}" - {{site_title}}' + replied: '{{username}} trả lời cho bạn trong "{{topic}}" - {{site_title}}' + posted: '{{username}} gửi bài trong "{{topic}}" - {{site_title}}' + private_message: '{{username}} đã gửi cho bạn một tin nhắn trong "{{topic}}" - {{site_title}}' + linked: '{{username}} liên quan đến bài viết của bạn từ "{{topic}}" - {{site_title}}' + upload_selector: + title: "Thêm một ảnh" + title_with_attachments: "Thêm một ảnh hoặc tệp tin" + from_my_computer: "Từ thiết bị của tôi" + from_the_web: "Từ Web" + remote_tip: "đường dẫn tới hình ảnh" + local_tip: "chọn hình từ thiết bị của bạn" + hint: "(Bạn cũng có thể kéo & thả vào trình soạn thảo để tải chúng lên)" + hint_for_supported_browsers: "bạn có thể kéo và thả ảnh vào trình soan thảo này" + uploading: "Đang tải lên" + select_file: "Chọn Tài liệu" + image_link: "liên kết hình ảnh của bạn sẽ trỏ đến" + search: + sort_by: "Sắp xếp theo" + relevance: "Độ phù hợp" + latest_post: "Bài viết mới nhất" + most_viewed: "Xem nhiều nhất" + most_liked: "Like nhiều nhất" + select_all: "Chọn tất cả" + clear_all: "Xóa tất cả" + result_count: + other: "{{count}} kết quả cho \"{{term}}\"" + title: "tìm kiếm chủ đề, bài viết, tài khoản hoặc các danh mục" + no_results: "Không tìm thấy kết quả." + no_more_results: "Không tìm thấy kết quả" + search_help: Giúp đỡ tìm kiếm + searching: "Đang tìm ..." + post_format: "#{{post_number}} bởi {{username}}" + context: + user: "Tìm bài viết của @{{username}}" + category: "Tìm danh mục \"{{category}}\"" + topic: "Tìm trong chủ đề này" + private_messages: "Tìm tin nhắn" + hamburger_menu: "đi đến danh sách chủ đề hoặc danh mục khác" + new_item: "mới" + go_back: 'quay trở lại' + not_logged_in_user: 'Trang cá nhân với tóm tắt các hoạt động và cấu hình' + current_user: 'đi đến trang cá nhân của bạn' + topics: + bulk: + reset_read: "Đặt lại lượt đọc" + delete: "Xóa chủ đề" + dismiss_new: "Bỏ " + change_category: "Chuyển chuyên mục" + close_topics: "Đóng các chủ đề" + archive_topics: "Chủ đề Lưu trữ" + notification_level: "Thay đổi cấp độ thông báo" + choose_new_category: "Chọn chuyên mục mới cho chủ đề này:" + selected: + other: "Bạn đã chọn {{count}} chủ đề" + none: + unread: "Bạn không có chủ đề nào chưa đọc." + new: "Bạn không có chủ đề mới nào." + read: "Bạn vẫn chưa đọc bất kì chủ đề nào." + posted: "Bạn vẫn chưa đăng bài trong bất kì một chủ đề nào" + latest: "Chán quá. Chẳng có chủ đề mới nào hết trơn." + hot: "Không có chủ đề nào nổi bật." + bookmarks: "Bạn chưa chủ đề nào được đánh dấu." + category: "Không có chủ đề nào trong {{category}} ." + top: "Không có chủ đề top." + search: "Không có kết quả tìm kiếm." + educate: + new: '

Chủ đề mới của bạn nằm ở đây.

Mặc định, chủ đề được coi là mới và sẽ hiện một chỉ báo new nếu chúng được tạo trong 2 ngày vừa qua.

Bạn có thể thay đổi cái này trong preferences.

' + unread: '

Những chủ đề chưa đọc của bạn nằm ở đây.

Mặc định, chủ đề được coi là chưa đọc và sẽ hiện số lượng chưa đọc 1 nếu bạn:

  • Đã tạo chủ đề
  • Đã phản hồi chủ đề
  • Đọc chủ đề lâu hơn 4 phút

Hoặc nếu bạn đặt chủ đề là Đã theo dấu hoặc Đã xem qua điều khiển thông bao tại cuối mỗi chủ đề.

Bạn có thể thay đổi điều này trong preferences.

' + bottom: + latest: "Không còn thêm chủ đề nào nữa." + hot: "Không còn của đề nổi bật nào nữa." + posted: "Ở đây không có thêm chủ đề nào được đăng." + read: "Không còn thêm chủ đề chưa đọc nào nữa." + new: "Không còn thêm chủ đề mới nào nữa." + unread: "Không còn thêm chủ đề chưa đọc nào nữa." + category: "Không còn thêm chủ đề nào trong {{category}} ." + top: "Không còn của đề top nào nữa." + bookmarks: "Không còn thêm chủ đề được đánh dấu nào nữa." + search: "Không có thêm kết quả tìm kiếm nào nữa." + topic: + unsubscribe: + stop_notifications: "Từ bây giờ bạn sẽ không nhận thông báo từ {{title}}" + change_notification_state: "Tình trạn thông báo của bạn là" + filter_to: "{{post_count}} bài đăng trong chủ đề" + create: 'Chủ đề Mới' + create_long: 'Tạo một Chủ đề mới' + private_message: 'Bắt đầu một thông điệp' + list: 'Chủ đề' + new: 'chủ đề mới' + unread: 'chưa đọc' + new_topics: + other: '{{count}} chủ đề mới.' + unread_topics: + other: '{{count}} chủ đề chưa đọc.' + title: 'Chủ đề' + invalid_access: + title: "Chủ đề này là riêng tư" + description: "Xin lỗi, bạn không có quyền truy cập vào chủ đề đó!" + login_required: "Bạn cần phải đăng nhập để xem chủ đề đó" + server_error: + title: "Tải chủ đề thất bại" + description: "Xin lỗi, chúng tôi không thể tải chủ đề, có thể do kết nối có vấn đề. Xin hãy thử lại. Nếu vấn đề còn xuất hiện, hãy cho chúng tôi biết" + not_found: + title: "Không tìm thấy chủ đề" + description: "Xin lỗi, chúng tôi không thể tìm thấy chủ đề đó. Có lẽ nó đã bị loại bởi mod?" + total_unread_posts: + other: "Bạn có {{number}} bài đăng chưa đọc trong chủ đề này" + unread_posts: + other: "bạn có {{number}} bài đăng củ chưa đọc trong chủ đề này" + new_posts: + other: "có {{count}} bài đăng mới trong chủ đề này từ lần đọc cuối" + likes: + other: "có {{count}} thích trong chủ để này" + back_to_list: "Quay lại danh sách chủ đề" + options: "Các lựa chọn chủ đề" + show_links: "Hiển thị liên kết trong chủ đề này" + toggle_information: "chuyển đổi các chi tiết chủ để" + read_more_in_category: "Muốn đọc nữa? Xem qua các chủ đề khác trong {{catLink}} hoặc {{latestLink}}" + read_more: "Muốn đọc nữa? {{catLink}} hoặc {{latestLink}}" + read_more_MF: "Có { UNREAD, plural, =0 {} one { is 1 unread } other { are # unread } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {is } other{}} 1 new topic} other { {BOTH, select, true{and } false {are } other{}} # new topics} } remaining, or {CATEGORY, select, true {browse other topics in {catLink}} false {{latestLink}} other {}}" + browse_all_categories: Duyệt tất cả các hạng mục + view_latest_topics: xem các chủ đề mới nhất + suggest_create_topic: Tại sao không tạo một chủ đề mới? + jump_reply_up: nhảy đến những trả lời trước đó + jump_reply_down: nhảy tới những trả lời sau đó + deleted: "Chủ đề này đã bị xóa" + auto_close_notice: "Chủ đề này sẽ tự động đóng %{timeLeft}." + auto_close_notice_based_on_last_post: "Chủ đề này sẽ đóng %{duration} sau trả lời cuối cùng." + auto_close_title: 'Tự động-Đóng các Cài đặt' + auto_close_save: "Lưu" + auto_close_remove: "Đừng Tự Động-Đóng Chủ Đề Này" + progress: + title: tiến trình của chủ đề + go_top: "trên cùng" + go_bottom: "dưới cùng" + go: "đi tới" + jump_bottom: "nhảy tới bài viết cuối cùng" + jump_bottom_with_number: "nhảy tới bài viết %{post_number}" + total: tổng số bài viết + current: bài viết hiện tại + position: "bài viết %{current} trong %{total}" + notifications: + reasons: + '3_6': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang xem chuyên mục nàyotification' + '3_5': 'Bạn sẽ nhận được các thông báo bởi vì bạn đã bắt đầu xem chủ đề này một cách tự động' + '3_2': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang xem chủ đề này' + '3_1': 'Bạn sẽ được nhận thông báo bởi bạn đã tạo chủ để này.' + '3': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang xem chủ đề này' + '2_8': 'Bạn sẽ nhận được thông báo bởi vì bạn đang theo dõi chuyên mục này.' + '2_4': 'Bạn sẽ nhận được các thông báo bởi vì bạn đã đăng một trả lời vào chủ đề này' + '2_2': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang theo dõi chủ đề này.' + '2': 'Bạn sẽ nhận được các thông báo bởi vì bạn đọc chủ đề này ' + '1_2': 'Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn' + '1': 'Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn' + '0_7': 'Bạn đang bỏ qua tất cả các thông báo trong chuyên mục này' + '0_2': 'Bạn đang bỏ qua tất cả các thông báo trong chủ đề này' + '0': 'Bạn đang bỏ qua tất cả các thông báo trong chủ đề này' + watching_pm: + title: "Đang xem" + description: "Bạn sẽ được thông báo về từng trả lời mới trong tin nhắn này, và một số trả lời mới sẽ được hiển thị" + watching: + title: "Dang theo dõi" + description: "Bạn sẽ được thông báo về từng trả lời mới trong tin nhắn này, và một số trả lời mới sẽ được hiển thị" + tracking_pm: + title: "Đang theo dõi" + description: "Một số trả lời mới sẽ được hiển thị trong tin nhắn này. Bạn sẽ được thông báo nếu ai đó đề cập đến @tên của bạn hoặc trả lời bạn" + tracking: + title: "Đang theo dõi" + description: "Một số trả lời mới sẽ được hiển thị trong chủ đề này. Bạn sẽ được thông báo nếu ai đó đề cập đến @tên của bạn hoặc trả lời bạn" + regular: + title: "Bình thường" + regular_pm: + title: "Bình thường" + muted_pm: + title: "Im lặng" + description: "Bạn sẽ không bao giờ được thông báo về bất cứ điều gì về tin nhắn này. " + muted: + title: "Im lặng" + actions: + recover: "Không-Xóa Chủ Đề Này" + delete: "Xóa-Chủ Đề Này" + open: "Mở Chủ Đề" + close: "Đóng Chủ Đề" + multi_select: "Chọn Bài Viết..." + auto_close: "Tự Động Đóng..." + pin: "Ghim Chủ Đề..." + unpin: "Bỏ-Ghim Chủ Đề..." + unarchive: "Chủ đề Không Lưu Trữ" + archive: "Chủ Đề Lưu Trữ" + reset_read: "Đặt lại dữ liệu đọc" + feature: + pin: "Ghim Chủ Đề" + unpin: "Bỏ-Ghim Chủ Đề" + pin_globally: "Ghim Chủ Đề Tổng Thể" + make_banner: "Banner chủ đề" + remove_banner: "Bỏ banner chủ đề" + reply: + title: 'Trả lời' + help: 'bắt đầu soạn một trả lời mới cho chủ đề này' + clear_pin: + title: "Xóa ghim" + help: "Xóa trạng thái ghim của chủ đề này để nó không còn xuất hiện trên cùng danh sách chủ đề của bạn" + share: + title: 'Chia sẻ' + help: 'Chia sẻ một liên kết đến chủ đề này' + flag_topic: + title: 'Gắn cờ' + help: 'đánh dấu riêng tư chủ đề này cho sự chú ý hoặc gửi một thông báo riêng về nó' + success_message: 'Bạn đã đánh dấu thành công chủ đề này' + feature_topic: + confirm_pin: "Bạn đã có {{count}} chủ đề được ghim. Qúa nhiều chủ đề được ghim có thể là một trở ngại cho những thành viên mới và thành viên ẩn danh. Bạn có chắc chắn muốn ghim chủ đề khác trong chuyên mục này?" + unpin: "Xóa chủ đề này từ phần trên cùng của chủ đề {{categoryLink}}" + pin_note: "Người dùng có thể bỏ ghim chủ đề riêng cho mình" + pin_validation: "Ngày được yêu câu để gắn chủ đề này" + unpin_globally: "Bỏ chủ đề này khỏi phần trên cùng của danh sách tất cả các chủ đề" + global_pin_note: "Người dùng có thể bỏ ghim chủ đề riêng cho mình" + inviting: "Đang mời..." + invite_private: + email_or_username_placeholder: "địa chỉ thư điện tử hoặc tên người dùng" + action: "Mời" + error: "Xin lỗi, có lỗi khi mời người dùng này." + group_name: "Nhóm tên" + invite_reply: + title: 'Mời' + username_placeholder: "tên người dùng" + action: 'Gửi Lời Mời' + to_forum: "Chúng tôi sẽ gửi một email tóm tắt cho phép bạn của bạn gia nhập trực tiệp bằng cách nhấp chuột vào một đường dẫn, không cần phải đăng nhập." + sso_enabled: "Nhập tên đăng nhập hoặc địa chỉ email của người mà bạn muốn mời vào chủ đề này." + to_topic_blank: "Nhập tên đăng nhập hoặc địa chỉ email của người bạn muốn mời đến chủ đề này." + email_placeholder: 'name@example.com' + login_reply: 'Đăng nhập để trả lời' + filters: + n_posts: + other: "{{count}} bài viết" + cancel: "Bỏ đièu kiện lọc" + split_topic: + title: "Di chuyển tới Chủ đề mới" + action: "di chuyển tới chủ đề mới" + topic_name: "Tên chủ đề mới" + error: "Có lỗi khi di chuyển bài viết tới chủ đề mới." + merge_topic: + title: "Di chuyển tới chủ đề đang tồn tại" + action: "di chuyển tới chủ đề đang tồn tại" + error: "Có lỗi khi di chuyển bài viết đến chủ đề này." + change_owner: + title: "Chuyển chủ sở hữu bài viết" + action: "chuyển chủ sở hữu" + label: "Chủ sở hữ mới của Bài viết" + placeholder: "tên đăng nhập của chủ sở hữu mới" + change_timestamp: + title: "Đổi Timestamp" + action: "đổi timestamp" + invalid_timestamp: "Timestamp không thể trong tương lai." + error: "Có lỗi khi thay đổi timestamp của chủ đề." + multi_select: + select: 'chọn' + selected: 'đã chọn ({{count}})' + select_replies: 'chọn + trả lời' + delete: xóa lựa chọn + cancel: hủy lựa chọn + select_all: chọn tất cả + deselect_all: bỏ chọn tất cả + description: + other: Bạn đã chọn {{count}} bài viết. + post: + reply: " {{replyAvatar}} {{usernameLink}}" + reply_topic: " {{link}}" + quote_reply: "trả lời trích dẫn" + edit: "Đang sửa {{link}} {{replyAvatar}} {{username}}" + edit_reason: "Lý do: " + post_number: "bài viết {{number}}" + last_edited_on: "đã sửa bài viết lần cuối lúc" + reply_as_new_topic: "Trả lời như là liên kết đến Chủ đề" + continue_discussion: "Tiếp tục thảo luận từ {{postLink}}:" + follow_quote: "đến bài viết trích dẫn" + show_full: "Hiển thị đầy đủ bài viết" + show_hidden: 'Xem nội dung ẩn' + expand_collapse: "mở/đóng" + gap: + other: "xem {{count}} trả lời bị ẩn" + more_links: "hơn {{count}}..." + unread: "Bài viết chưa đọc" + has_replies: + other: "{{count}} Trả lời" + has_likes: + other: "{{count}} Thích" + has_likes_title: + other: "{{count}} người thích bài viết này" + errors: + create: "Xin lỗi, có lỗi xảy ra khi tạo bài viết của bạn. Vui lòng thử lại." + edit: "Xin lỗi, có lỗi xảy ra khi sửa bài viết của bạn. Vui lòng thử lại." + upload: "Xin lỗi, có lỗi xảy ra khi tải lên tập tin này. Vui lòng thử lại." + attachment_too_large: "Xin lỗi, tập tin của bạn tải lên quá lớn (kích thước tối đa là {{max_size_kb}}kb)." + file_too_large: "Xin lỗi, tập tin của bạn tải lên quá lớn (kích thước tối đa là {{max_size_kb}}kb)" + too_many_uploads: "Xin lỗi, bạn chỉ có thể tải lên 1 file cùng 1 lúc." + too_many_dragged_and_dropped_files: "Xin lỗi, bạn chỉ có thể kéo và thả 10 tập tin cùng lúc." + upload_not_authorized: "Xin lỗi, tập tin của bạn tải lên chưa được cho phép (định dạng cho phép: {{authorized_extensions}})." + image_upload_not_allowed_for_new_user: "Xin lỗi, tài khoản mới không thể tải lên ảnh." + attachment_upload_not_allowed_for_new_user: "Xin lỗi, tài khoản mới không thể tải lên đính kèm." + attachment_download_requires_login: "Xin lỗi, bạn cần đăng nhập để tải về đính kèm." + abandon: + confirm: "Bạn có chắc muốn bỏ bài viết của bạn?" + no_value: "Không, giữ lại" + yes_value: "Đồng ý, bỏ" + via_email: "bài viết này đăng qua email" + whisper: "bài viết này là lời nhắn từ điều hành viên" + wiki: + about: "bài viết này là wiki; người dùng cơ bản có thể sửa nó" + archetypes: + save: 'Lưu lựa chọn' + controls: + reply: "bắt đầu soản trả lời cho bài viết này" + like: "like bài viết này" + has_liked: "bạn đã like bài viết này" + undo_like: "hủy like" + edit: "sửa bài viết này" + edit_anonymous: "Xin lỗi, nhưng bạn cần đăng nhập để sửa bài viết này." + delete: "xóa bài viết này" + undelete: "hủy xóa bài viết này" + share: "chia sẻ liên kết đến bài viết này" + more: "Thêm" + delete_replies: + confirm: + other: "Bạn muốn xóa {{count}} trả lời cho bài viết này?" + yes_value: "Đồng ý, xóa những trả lời" + no_value: "Không, chỉ xóa chủ đề" + wiki: "Tạo Wiki" + unwiki: "Xóa Wiki" + convert_to_moderator: "Thêm màu Nhân viên" + revert_to_regular: "Xóa màu Nhân viên" + rebake: "Tạo lại HTML" + unhide: "Bỏ ẩn" + actions: + flag: 'Gắn cờ' + it_too: + off_topic: "Gắn cờ nó" + spam: "Gắn cờ nó" + inappropriate: "Gắn cờ nó" + custom_flag: "Gắn cờ nó" + bookmark: "Đánh dấu nó" + like: "Thích nó" + vote: "Bịnh chọn nó" + undo: + off_topic: "Hủy gắn cờ" + spam: "Hủy gắn cờ" + inappropriate: "Hủy gắn cờ" + bookmark: "Hủy đánh dấu" + like: "Hủy like" + vote: "Hủy bình chọn" + people: + off_topic: "{{icons}} đánh dấu nói sai chủ đề" + spam: "{{icons}} đánh dấu nó là rác" + spam_with_url: "{{icons}} gắn cờ cái này là rác" + inappropriate: "{{icons}} gắn cờ là không phù hợp" + notify_moderators: "{{icons}} thông báo cho điều hành viên" + notify_moderators_with_url: "{{icons}} đã thông báo quản trị viên" + notify_user: "{{icons}} gửi một tin nhắn" + notify_user_with_url: "{{icons}} gửi một tin nhắn" + bookmark: "{{icons}} đã đánh dấu" + like: "{{icons}} thích cái này" + vote: "{{icons}} bình chọn cho cái này" + by_you: + off_topic: "Bạn đã đánh dấu cái nfay là chủ đề đóng" + spam: "Bạn đã đánh dấu cái này là rác" + inappropriate: "Bạn đã đánh dấu cái này là không phù hợp" + notify_moderators: "Bạn đã đánh dấu cái này cho điều tiết" + notify_user: "Bạn đã gửi một tin nhắn đến người dùng này" + bookmark: "Bạn đã đánh dấu bài viết này" + like: "Bạn đã thích cái này" + vote: "Bạn đã bình chọn cho bài viết này" + by_you_and_others: + off_topic: + other: "Bạn và {{count}} người khác đã đánh dấu đây là chủ đề đóng" + spam: + other: "Bạn và {{count}} người khác gắn cờ nó là rác" + inappropriate: + other: "Bạn và {{count}} other người khác đã đánh dấu nó là không phù hợp" + notify_moderators: + other: "Bạn và {{count}} người khác gắn cờ nó là điều tiết" + notify_user: + other: "Bạn và {{count}} người khác đã gửi một tin nhắn đến người dùng này" + bookmark: + other: "Bạn và {{count}} người khác đã đánh dấu bài viết này" + like: + other: "Bạn và {{count}} người khác đã thích cái này" + vote: + other: "Bạn và {{count}} nười khác đã bình chọn cho bài viết này" + by_others: + off_topic: + other: "{{count}} người đã đánh dấu nó là chủ đề đóng" + spam: + other: "{{count}} người khác đánh dấu là rác" + inappropriate: + other: "{{count}} người khác đã đánh dấu là không phù hợp" + notify_user: + other: "{{count}} gửi tin nhắn đến người dùng này" + bookmark: + other: "{{count}} người đã đánh dấu bài viết này" + like: + other: "{count}} người đã thích cái này" + vote: + other: "{{count}} người đã bình chọn cho bài viết này" + delete: + confirm: + other: "Bạn muốn xóa những bài viết này?" + revisions: + controls: + first: "Sửa đổi đầu tiên" + previous: "Sửa đổi trước" + next: "Sửa đổi tiếp theo" + last: "Sửa đổi gần nhất" + hide: "Ẩn sửa đổi" + show: "Hiện sửa đổi" + displays: + inline: + button: ' HTML' + side_by_side: + button: ' HTML' + side_by_side_markdown: + button: ' Thô' + category: + can: 'can…' + none: '(không danh mục)' + all: 'Tất cả danh mục' + edit: 'sửa' + edit_long: "Sửa" + view: 'Xem Chủ đề trong Danh mục' + general: 'Chung' + settings: 'Cấu hình' + topic_template: "Mẫu Chủ đề" + delete: 'Xóa chuyên mục' + create: 'Chuyên mục mới' + create_long: 'Tạo Chủ đề mới' + save: 'Lưu chuyên mục' + creation_error: Có lỗi xảy ra khi tạo chuyên mục + save_error: Có lỗi xảy ra khi lưu chuyên mục + name: "Tên chuyên mục" + description: "Mô tả" + topic: "chủ đề chuyên mục" + logo: "Logo của chuyên mục" + background_image: "Ảnh nền của chuyên mục" + background_color: "Màu nền" + name_placeholder: "Tối đa một hoặc hai từ" + color_placeholder: "Bất cứ màu nào" + delete_confirm: "Bạn có chắc sẽ xóa chuyên mục này chứ?" + delete_error: "Có lỗi xảy ra khi xóa chuyên mục này" + list: "Danh sách chuyên mục" + no_description: "Hãy thêm mô tả cho chuyên mục này" + change_in_category_topic: "Sửa mô tả" + already_used: 'Màu này đã được dùng bởi chuyên mục khác' + security: "Bảo mật" + images: "Hình ảnh" + auto_close_label: "Tự động khóa chủ đề sau:" + auto_close_units: "giờ" + email_in: "Tùy chỉnh địa chỉ nhận thư điện tử " + email_in_allow_strangers: "Nhận thư điện tử từ người gửi vô danh không tài khoản" + email_in_disabled_click: 'kích hoạt thiết lập thư điện tử' + allow_badges_label: "Cho phép thưởng huy hiệu trong chuyên mục này" + edit_permissions: "Sửa quyền" + add_permission: "Thêm quyền" + this_year: "năm nay" + position: "vị trí" + default_position: "vị trí mặc định" + parent: "Danh mục cha" + notifications: + watching: + title: "Theo dõi" + tracking: + title: "Đang theo dõi" + regular: + title: "Bình thường" + muted: + title: "Im lặng" + flagging: + action: 'Đánh dấu Bài viết' + notify_action: 'Tin nhắn' + delete_spammer: "Xóa người Spam" + delete_confirm: "Bạn đang định xóa %{posts} bài đăng và %{topics} chủ đề từ người dùng này, loại tài khoản, ngăn đăng ký từ địa chỉ IP %{ip_address} của họ, và thêm địa chỉ email %{email} vào danh sách chặn vĩnh viễn. Bạn có chắc người dùng này thật sự là một spammer?" + ip_address_missing: "(N/A)" + hidden_email_address: "(ẩn)" + formatted_name: + off_topic: "Nó là sai chủ đề" + spam: "Nó là rác" + custom_message: + more: "còn {{n}}" + flagging_topic: + action: "Gắn cờ Chủ đề" + notify_action: "Tin nhắn" + topic_map: + title: "Tóm tắt Chủ đề" + links_title: "Liên kết phổ biến" + clicks: + other: "%{count} nhấp chuột" + topic_statuses: + warning: + help: "Đây là một cảnh báo chính thức." + bookmarked: + help: "Bạn đã đánh dấu chủ đề này" + locked: + help: "Chủ đề đã đóng; không cho phép trả lời mới" + unpinned: + title: "Hủy gắn" + pinned: + title: "Gắn" + posts: "Bài viết" + posts_lowercase: "bài viết" + posts_long: "Có {{number}} bài đăng trong chủ đề này" + posts_likes_MF: | + Chủ đề này có {count, plural, one {1 reply} other {# replies}} {ratio, select, + low {with a high like to post ratio} + med {with a very high like to post ratio} + high {with an extremely high like to post ratio} + other {}} + original_post: "Bài viết gốc" + views: "Lượt xem" + views_lowercase: + other: "lượt xem" + replies: "Trả lời" + views_long: "chủ đề đã được xem {{number}} lần" + activity: "Hoạt động" + likes: "Lượt thích" + likes_lowercase: + other: "lượt thích" + likes_long: "Có {{number}} thích trong chủ đề này" + users: "Người dùng" + users_lowercase: + other: "người dùng" + category_title: "Danh mục" + history: "Lịch sử" + changed_by: "bởi {{author}}" + raw_email: + title: "Email gốc" + not_available: "Không sẵn sàng!" + categories_list: "Danh sách Danh mục" + filters: + with_topics: "%{filter} chủ đề" + with_category: "%{filter} %{category} chủ đề" + latest: + help: "chủ đề với bài viết gần nhất" + hot: + title: "Nổi bật" + read: + title: "Đọc" + search: + title: "Tìm kiếm" + help: "tìm trong tất cả chủ đề" + categories: + title: "Danh mục" + title_in: "Danh mục - {{categoryName}}" + new: + lower_title: "mới" + help: "chủ đề đã tạo cách đây vài ngày" + posted: + title: "Bài viết của tôi" + help: "chủ đề của bạn đã được đăng trong" + bookmarks: + title: "Đánh dấu" + help: "chủ để của bạn đã được đánh dấu" + category: + help: "Những chủ đề mới nhất trong chuyên mục{{categoryName}} " + top: + title: "Trên" + all: + title: "Từ trước tới nay" + yearly: + title: "Hàng năm" + quarterly: + title: "Hàng quý" + monthly: + title: "Hàng tháng" + weekly: + title: "Hàng tuần" + daily: + title: "Hàng ngày" + all_time: "Từ trước tới nay" + this_year: "Năm" + this_quarter: "Quý" + this_month: "Tháng" + this_week: "Tuần" + today: "Ngày" + other_periods: "xem top" + permission_types: + full: "Tạo / Trả lời / Xem" + create_post: "Trả lời / Xem" + readonly: "Xem" + admin_js: + type_to_filter: "gõ để lọc..." + admin: + title: 'Quản trị Diễn đàn' + moderator: 'Điều hành' + dashboard: + title: "Bảng điều khiển" + last_updated: "Bảng điều khiển cập nhật gần nhất:" + version: "Phiên bản" + up_to_date: "Bạn đã cập nhật phiên bản mới nhất" + critical_available: "Bản cập nhật quan trọng sẵn sằng." + updates_available: "Cập nhật đang sẵng sàng" + please_upgrade: "Vui lòng cập nhật!" + installed_version: "Đã cài đặt" + latest_version: "Mới nhất" + problems_found: "Tìm thấy vấn đề với bản cài đặt Discourse của bạn:" + last_checked: "Kiểm tra lần cuối" + refresh_problems: "Làm mới" + no_problems: "Không phát hiện vấn đề" + moderators: 'Điều hành:' + admins: 'Quản trị:' + blocked: 'Đã khóa:' + suspended: 'Đã tạm khóa:' + private_messages_short: "Tin nhắn" + private_messages_title: "Tin nhắn" + mobile_title: "Điện thoại" + space_free: "{{size}} trống" + uploads: "tải lên" + backups: "sao lưu" + traffic_short: "Băng thông" + traffic: "Application web requests" + page_views: "API Requests" + page_views_short: "API Requests" + show_traffic_report: "Xem chi tiết Báo cáo Lưu lượng" + reports: + today: "Hôm nay" + yesterday: "Hôm qua" + last_7_days: "7 Ngày gần nhất" + last_30_days: "30 Ngày gần nhất" + all_time: "Từ trước tới nay" + 7_days_ago: "7 Ngày trước" + 30_days_ago: "30 Ngày trước" + all: "Tất cả" + view_table: "bảng" + view_chart: "biểu đồ bar" + refresh_report: "Làm mới báo cáo" + start_date: "Từ ngày" + end_date: "Đến ngày" + commits: + latest_changes: "Thay đổi cuối: vui lòng cập nhật thường xuyên!" + by: "bởi" + flags: + title: "Gắn cờ" + old: "Cũ" + active: "Kích hoạt" + agree: "Đồng ý" + agree_flag_modal_title: "Đồng ý và..." + agree_flag_hide_post: "Đồng ý (ẩn bài viết + gửi PM)" + agree_flag_hide_post_title: "Ẩn bài viết này và tự động gửi tin nhắn đến người dùng hối thúc họ sửa nó" + agree_flag_restore_post: "Đồng ý (khôi phục bài viết)" + agree_flag_restore_post_title: "Khôi phục bài viết này" + agree_flag: "Đống ý với cờ này" + agree_flag_title: "Đồng ý với cờ này và giữ bài viết không thay đổi" + defer_flag: "Hoãn" + defer_flag_title: "Xóa cờ này; nó yêu cầu không có hành động nào vào thời điểm này." + delete: "Xóa" + delete_post_defer_flag_title: "Xóa bài viết; nếu là bài viết đầu tiên, xóa chủ đề này" + delete_post_agree_flag: "Xóa bài viết và Đồng ý với cờ" + delete_post_agree_flag_title: "Xóa bài viết; nếu là bài viết đầu tiên, xóa chủ đề này" + delete_flag_modal_title: "Xóa và..." + delete_spammer: "Xóa người Spam" + delete_spammer_title: "Xóa người dùng này và tất cả bài viết à chủ để của người dùng này." + disagree_flag_unhide_post: "Không đồng ý (ẩn bài viết)" + disagree_flag: "Không đồng ý" + clear_topic_flags: "Hoàn tất" + more: "(thêm trả lời...)" + dispositions: + agreed: "đồng ý" + disagreed: "không đồng ý" + deferred: "hoãn" + flagged_by: "Gắn cờ bởi" + system: "Hệ thống" + error: "Có lỗi xảy ra" + reply_message: "Trả lời " + no_results: "Không được gắn cờ" + summary: + action_type_3: + other: "sai chủ đề x{{count}}" + groups: + primary: "Nhóm Chính" + no_primary: "(không có nhóm chính)" + title: "Nhóm" + edit: "Sửa nhóm" + refresh: "Làm mới" + new: "Mới" + selector_placeholder: "nhập tên tài khoản" + name_placeholder: "Tên nhóm, không khoản trắng, cùng luật với tên tài khoản" + group_members: "Nhóm thành viên" + delete: "Xóa" + delete_confirm: "Xóa nhóm này?" + name: "Tên" + add: "Thêm" + add_members: "Thêm thành viên" + custom: "Tùy biến" + automatic: "Tự động" + primary_group: "Tự động cài là nhóm chính" + api: + generate_master: "Tạo Master API Key" + none: "Không có API keys nào kích hoạt lúc này." + user: "Thành viên" + title: "API" + key: "API Key" + generate: "Khởi tạo" + regenerate: "Khởi tạo lại" + revoke: "Thu hồi" + confirm_regen: "Bạn muốn thay API Key hiện tại bằng cái mới?" + all_users: "Tất cả Thành viên" + note_html: "Giữ khóa nào bảo mật, tất cả tài khoản có thể dùng khóa này để tạo bài viết với bất kỳ tài khoản nào." + plugins: + title: "Plugin" + installed: "Đã cài Plugin" + name: "Tên" + none_installed: "Bạn chưa cài plugin nào." + version: "Phiên bản" + enabled: "Kích hoạt" + is_enabled: "Có" + not_enabled: "Không" + change_settings: "Đổi Cấu hình" + change_settings_short: "Cấu hình" + howto: "Plugin cài như thế nào?" + backups: + title: "Bản sao lưu" + menu: + backups: "Bản sao lưu" + logs: "Log" + none: "Chưa có bản sao lưu." + read_only: + enable: + title: "Kích hoạt chế độ chỉ xem" + label: "Kích hoạt chế độ chỉ xem" + confirm: "Bạn muốn kích hoạt chế chộ chỉ xem?" + disable: + title: "Hủy chế độ chỉ xem này" + label: "Hủy chế độ chỉ xem" + logs: + none: "Chưa có log..." + columns: + filename: "Tên tập tin" + size: "Kích thước" + upload: + label: "Tải lên" + title: "Tải lên bản sao lưu cho phiên bản này" + uploading: "Đang tải lên..." + success: "'{{filename}}' đã tải lên thành công." + error: "Có lõi trong quá trình tải lên '{{filename}}': {{message}}" + operations: + is_running: "Tác vụ đang chạy..." + failed: "{{operation}} Thấy bại. Vui lòng xem log." + cancel: + label: "Hủy" + title: "Hủy tác vụ hiện tại" + confirm: "Bạn muốn hủy tác vụ hiện tại?" + backup: + label: "Sao lưu" + title: "Tạo bản sao lưu" + confirm: "Bạn muốn bắt đầu một bản sao lưu mới?" + without_uploads: "Đúng (không bao gồm những tập tin)" + download: + label: "Tải xuống" + title: "Tải xuống bản sao lưu này" + destroy: + title: "Xóa bản sao lưu này" + confirm: "Bạn muốn hủy bản sao lưu này?" + restore: + is_disabled: "Khôi phục đã bị cấm sử dụng trong cấu hình trang." + label: "Khôi phục" + title: "Khôi phục lại sao lưu này" + confirm: "Bạn muốn khôi phục bản sao lưu này?" + rollback: + label: "Rollback" + export_csv: + failed: "Xuất lỗi. Vui lòng kiểm tra log." + rate_limit_error: "Bài viết có thể tải về 1 lần mỗi này, vui lòng thử lại vào ngày mai." + button_text: "Xuất" + button_title: + user: "Xuất danh sách người dùng đầy đủ với định dạng CSV." + staff_action: "Xuất đầy đủ log hành động của nhân viên với định dạng CSV." + export_json: + button_text: "Xuất" + invite: + button_text: "Gửi Lời Mời" + button_title: "Gửi Lời Mời" + customize: + title: "Tùy biến" + long_title: "Tùy biến trang" + css: "CSS" + header: "Header" + top: "Trên" + footer: "Footer" + embedded_css: "Nhúng CSS" + head_tag: + text: "" + title: "HTML sẻ thêm trước thẻ " + body_tag: + text: "" + title: "HTML sẽ thêm trước thẻ " + override_default: "Không bao gồm style sheet chuẩn" + enabled: "Cho phép?" + preview: "xem trước" + undo_preview: "xóa xem trước" + save: "Lưu" + new: "Mới" + import: "Nhập" + import_title: "Chọn một file hoặc paste chữ." + delete: "Xóa" + delete_confirm: "Xóa tùy biến này?" + about: "Chỉnh sửa CSS và HTML header trên trang. Thêm tùy biến để bắt đầu." + color: "Màu sắc" + opacity: "Độ mờ" + copy: "Sao chép" + css_html: + title: "CSS/HTML" + long_title: "Tùy biến CSS và HTML" + colors: + title: "Màu sắc" + long_title: "Bảng màu" + about: "Chỉnh " + new_name: "Bản màu mới" + copy_name_prefix: "Bản sao của" + delete_confirm: "Xóa bảng màu này?" + undo: "hoàn tác" + undo_title: "Hoàn tác thay đổi của bạn vơ" + revert: "phục hồi" + revert_title: "Thiết lập lại màu về mặc định của Discourse." + primary: + name: 'chính' + description: 'Hầu hết chữ, biểu tượng, và viền.' + secondary: + name: 'cấp hai' + description: 'Màu nền, và màu chữ của một vài nút.' + tertiary: + name: 'cấp ba' + description: 'Liên kết, một và nút, thông báo, và màu nhấn.' + header_background: + name: "nền header" + description: "Màu nền header của trang." + header_primary: + name: "header chính" + highlight: + name: 'highlight' + danger: + name: 'nguy hiểm' + success: + name: 'thành công' + love: + name: 'đáng yêu' + description: "Màu của nút like" + wiki: + name: 'wiki' + email: + title: "Email" + settings: "Cấu hình" + all: "Tất cả" + sending_test: "Đang gửi Email test..." + error: "LỖI - %{server_error}" + test_error: "Có vấn đề khi gửi email test. Vui lòng kiểm tra lại cấu hình email của bạn, chắc chắn host mail của bạn không bị khóa kết nối, và thử lại." + sent: "Đã gửi" + skipped: "Đã bỏ qua" + sent_at: "Đã gửi vào lúc" + time: "Thời gian" + user: "Thành viên" + email_type: "Loại Email" + to_address: "Đến Địa chỉ" + test_email_address: "địa chỉ email để test" + send_test: "Gửi Email test" + sent_test: "đã gửi!" + refresh: "Tải lại" + format: "Định dạng" + html: "html" + text: "text" + last_seen_user: "Người dùng cuối:" + reply_key: "Key phản hồi" + skipped_reason: "Bỏ qua Lý do" + logs: + none: "Không tìm thấy log." + filters: + title: "Lọc" + user_placeholder: "tên người dùng" + address_placeholder: "name@example.com" + reply_key_placeholder: "key phản hồi" + skipped_reason_placeholder: "lý do" + logs: + title: "Log" + action: "Hành động" + created_at: "Đã tạo" + ip_address: "IP" + topic_id: "ID Chủ đề" + post_id: "ID Bài viết" + category_id: "ID Danh mục" + delete: 'Xoá' + edit: 'Sửa' + save: 'Lưu' + screened_actions: + block: "khóa" + do_nothing: "không làm gì" + staff_actions: + clear_filters: "Hiện thị mọi thứ" + staff_user: "Tài khoản Nhân viên" + subject: "Chủ đề" + when: "Khi" + details: "Chi tiết" + previous_value: "Trước" + new_value: "Mới" + diff: "So sánh" + show: "Hiển thị" + modal_title: "Chi tiết" + actions: + delete_user: "xóa người dùng" + change_trust_level: "thay đổi cấp tin cậy" + change_username: "thay đổi username" + change_site_setting: "thay đổi cấu hình trang" + change_site_customization: "thay đổi tùy biến trang" + delete_site_customization: "xóa tùy biến trang" + check_email: "kiểm tra email" + delete_topic: "xóa chủ đề" + delete_post: "xóa bài viết" + change_category_settings: "thay đổi cấu hình danh mục" + delete_category: "xóa danh mục" + create_category: "tạo danh mục" + screened_emails: + email: "Địa chỉ Email" + actions: + allow: "Cho phép" + screened_urls: + url: "URL" + domain: "Tên miền" + screened_ips: + rolled_up_no_subnet: "Không có gì để cuộn lên." + actions: + block: "Khóa" + do_nothing: "Cho phép" + allow_admin: "Cho phép Quản trị" + form: + label: "Mới:" + ip_address: "Địa chỉ IP" + add: "Thêm" + filter: "Tìm kiếm" + roll_up: + text: "Cuộn lên" + logster: + title: "Log lỗi" + impersonate: + title: "Mạo danh" + not_found: "Không tìm thấy người dùng này." + users: + title: 'Tài khoản' + create: 'Thêm tài khoản Quản trị' + last_emailed: "Email trước đây" + not_found: "Xin lỗi, username không tồn tại trong hệ thống." + id_not_found: "Xin lỗi, id người dùng không tồn tại trong hệ thống." + active: "Kích hoạt" + show_emails: "Hiện địa chỉ Email" + nav: + new: "Mới" + active: "Kích hoạt" + pending: "Đang chờ xử lý" + staff: 'Nhân viên' + suspended: 'Đã tạm khóa' + blocked: 'Đã khóa' + approved: "Đã duyệt?" + approved_selected: + other: "duyệt tài khoản ({{count}})" + reject_selected: + other: "từ chối tài khoản ({{count}})" + titles: + active: 'Thành viên kích hoạt' + new: 'Thành viên mới' + pending: 'Hoãn Xem xét Tài khoản' + newuser: 'Tài khoản ở Cấp độ Tin tưởng 0 (Tài khoản mới)' + basic: 'Tài khoản ở Cấp độ Tin tưởng 1 (Tài khoản Cơ bản)' + staff: "Nhân viên" + admins: 'Tài khoản Quản trị' + moderators: 'Điều hành viên' + blocked: 'Tài khoản Khóa' + suspended: 'Tài khoản Tạm khóa' + reject_successful: + other: "Từ chối thành công %{count} tài khoản." + reject_failures: + other: "Từ chối thất bại %{count} tài khoản." + not_verified: "Chưa xác thực" + check_email: + text: "Hiển thị" + user: + suspend_duration_units: "(ngày)" + suspend_reason: "Lý do" + suspended_by: "Tạm khóa bởi" + delete_all_posts: "Xóa tất cả bài viết" + suspend: "Tạm khóa" + unsuspend: "Đã mở khóa" + suspended: "Đã tạm khóa?" + moderator: "Mod?" + admin: "Quản trị?" + blocked: "Đã khóa?" + show_admin_profile: "Quản trị" + edit_title: "Sửa Tiêu đề" + save_title: "Lưu Tiêu đề" + refresh_browsers_message: "Tin nhắn đã gửi cho tất cả người dùng!" + show_public_profile: "Hiển thị hồ sơ công khai" + impersonate: 'Mạo danh' + ip_lookup: "Tìm kiếm địa chỉ IP" + log_out: "Đăng suất" + logged_out: "Thành viên đã đăng xuất trên tất cả thiết bị" + unblock: 'Mở khóa' + block: 'Khóa' + reputation: Danh tiếng + permissions: Quyền + activity: Hoạt động + last_100_days: 'trong 100 ngày gần đây' + private_topics_count: Chủ đề riêng tư + posts_read_count: Đọc bài viết + post_count: Bài đăng đã được tạo + topics_entered: Chủ để đã xem + warnings_received_count: Đã nhận Cảnh báo + approve: 'Duyệt' + approved_by: "duyệt bởi" + approve_success: "Thành viên được duyệt và đã gửi email hướng đẫn kích hoạt." + approve_bulk_success: "Thành công! Tất cả thành viên đã chọn được duyệt và thông báo." + time_read: "Thời gian đọc" + anonymize: "Tài khoản Nặc danh" + anonymize_confirm: "Bạn CHĂC CHẮN muốn xóa tài khoản nặc danh này? Nó sẽ thay đổi tên đăng nhập và email, và xóa tất cả thông tin trong hồ sơ." + anonymize_yes: "Đồng ý, đây là tài khoản nặc danh." + anonymize_failed: "Có vấn đề với những tài khoản nặc danh." + delete: "Xóa thành viên" + delete_forbidden_because_staff: "Admin và mod không thể xóa." + delete_posts_forbidden_because_staff: "Không thể xóa tất cả bài viết của quản trị và điều hành viên." + delete_confirm: "Bạn CHẮC CHẮN muốn xóa thành viên này? Nó là vĩnh viễn!" + delete_and_block: "Xóa và khóa email này và địa chỉ IP" + delete_dont_block: "Chỉ xóa" + deleted: "Thành viên này đã bị xóa" + delete_failed: "Có lỗi trong quá trình xóa thành viên này. Chắc chắn rằng tất cả bài viết đã được xóa trước khi xóa thành viên." + send_activation_email: "Gửi email kích hoạt" + activation_email_sent: "Email kích hoạt đã được gửi." + send_activation_email_failed: "Có vấn đề khi gửi lại email kích hoạt. %{error}" + activate: "Kích hoạt tài khoản" + activate_failed: "Có vấn đề khi kích hoạt thành viên này." + deactivate_account: "Vô hiệu hóa Tài khoản" + deactivate_failed: "Có vấn đề khi bỏ kích hoạt thành viên này." + unblock_failed: 'Có vẫn đề khi gỡ khóa thành viên này.' + block_failed: 'Có vấn đề khi khóa thành viên này.' + suspended_explanation: "Tài khoản tạm khóa không thể đăng nhập." + block_explanation: "Tài khoản bị khóa không thể đăng bài hoặc tạo chủ đề." + trust_level_change_failed: "Có lỗi xảy ra khi thay đổi mức độ tin tưởng của tài khoản." + suspend_modal_title: "Tạm khóa Thành viên" + lock_trust_level: "Khóa Cấp độ Tin tưởng" + tl3_requirements: + title: "Yêu cầu Cấp độ tin tưởng 3" + value_heading: "Giá trị" + requirement_heading: "Yêu cầu" + visits: "Lượt xem" + days: "ngày" + topics_viewed: "Đã xem chủ đề" + topics_viewed_all_time: "Đã xem chủ đề (mọi lúc)" + posts_read: "Đọc bài viết" + posts_read_all_time: "Đọc bài viết (mọi lúc)" + flagged_posts: "Đã gắn cờ Bài viết" + sso: + title: "Single Sign On" + external_id: "ID Bên ngoài" + external_username: "Tên đăng nhập" + external_name: "Tên" + external_email: "Email" + external_avatar_url: "URL Ảnh đại diện" + user_fields: + untitled: "Không có tiêu đề" + name: "Tên Trường" + type: "Loại Trường" + description: "Trường mô tả" + save: "Lưu" + edit: "Sửa" + delete: "Xoá" + cancel: "Hủy" + delete_confirm: "Bạn muốn xóa trường thành viên?" + options: "Lựa chọn" + required: + title: "Bắt buộc lúc đăng ký?" + enabled: "bắt buộc" + disabled: "không bắt buộc" + editable: + title: "Có thể chỉnh sửa sau khi đăng ký?" + enabled: "có thể chỉnh sửa" + disabled: "không thể chỉnh sửa" + show_on_profile: + title: "Hiển thị trong hồ sơ công khai" + enabled: "hiển thị trong hồ sơ" + disabled: "không hiển thị trong hồ sơ" + field_types: + text: 'Nội dung chữ' + confirm: 'Xác nhận' + dropdown: "Xổ xuống" + site_text: + title: 'Nội Dung Chữ' + site_settings: + show_overriden: 'Chỉ hiện thị đã ghi đè' + title: 'Xác lập' + reset: 'trạng thái đầu' + none: 'không có gì' + no_results: "Không tìm thấy kết quả." + clear_filter: "Xóa" + add_url: "thêm URL" + add_host: "thêm host" + categories: + all_results: 'Tất cả' + required: 'Bắt buộc' + basic: 'Cài đặt cơ bản' + users: 'Thành viên' + posting: 'Đang đăng bài' + email: 'Email' + files: 'Tập tin' + trust: 'Độ tin tưởng' + security: 'Bảo mật' + onebox: "Onebox" + seo: 'SEO' + spam: 'Rác' + developer: 'Nhà phát triển' + uncategorized: 'Khác' + backups: "Sao lưu" + login: "Đăng nhập" + plugins: "Plugins" + user_preferences: "Tùy chỉnh Tài khoản" + badges: + new: Mới + name: Tên + display_name: Tên Hiển thị + description: Mô tả + badge_grouping: Nhóm + reason_help: (Liên kết đến bài viết hoặc chủ đề) + save: Lưu + delete: Xóa + reason: Lý do + icon: Biểu tượng + image: Hình ảnh + trigger_type: + none: "Cập nhật hàng ngày" + preview: + bad_count_warning: + header: "CẢNH BÁO!" + sample: "Ví dụ:" + grant: + with: %{username} + with_post: %{username} for post in %{link} + emoji: + name: "Tên" + image: "Hình ảnh" + embedding: + confirm_delete: "Bạn muốn xóa host này?" + host: "Cho phép Host" + edit: "sửa" + category: "Đăng vào Danh mục" + add_host: "Thêm Host" + feed_settings: "Cấu hình Feed" + crawling_settings: "Cấu hình Crawler" + embed_blacklist_selector: "CSS selector for elements that are removed from embeds" + feed_polling_enabled: "Nhập bài viết bằng RSS/ATOM" + permalink: + title: "Liên kết cố định" + url: "URL" + topic_id: "ID Chủ đề" + topic_title: "Chủ đề" + post_id: "ID Bài viết" + post_title: "Bài viết" + category_id: "ID Danh mục" + category_title: "Danh mục" + external_url: "URL Bên ngoài" + form: + label: "Mới:" + add: "Thêm" + filter: "Tìm kiếm (URL hoặc External URL)" + lightbox: + download: "tải" + search_help: + title: 'Tìm giúp đỡ' + keyboard_shortcuts_help: + title: 'Phím tắt' + jump_to: + title: 'Chuyển đến' + home: 'g, h Trang chủ' + latest: 'g, l Cuối cùng' + new: 'g, n Mới' + unread: 'g, u Chưa đọc' + categories: 'g, c Danh mục' + top: 'g, t Trên' + bookmarks: 'g, b Đánh dấu' + profile: 'g, p Hồ sơ' + messages: 'g, m Tin nhắn' + navigation: + title: 'Điều hướng' + jump: '# Đến bài viết #' + back: 'u Quay lại' + open: 'o or Enter Mở chủ để đã chọn' + next_prev: 'shift+j/shift+k Next/previous section' + application: + title: 'Ứng dụng' + create: 'c Tạo mới chủ đề' + notifications: 'n Mở thông báo' + user_profile_menu: 'p Mở trình đơn thành viên' + show_incoming_updated_topics: '. Show updated topics' + search: '/ Tìm kiếm' + dismiss_new_posts: 'x, r Dismiss New/Posts' + dismiss_topics: 'x, t Bỏ qua bài viết' + log_out: 'shift+z shift+z Đăng xuất' + actions: + title: 'Hành động' + pin_unpin_topic: 'shift+p Pin/Unpin bài viết' + share_topic: 'shift+s Chia sẻ bài viết' + share_post: 's Chia sẻ bài viết' + reply_as_new_topic: 't Trả lời như là một liên kết đến bài viết' + reply_topic: 'shift+r Trả lời bài viết' + reply_post: 'r Trả lời bài viết' + like: 'l Thích bài viết' + bookmark: 'b Đánh dấu bài viết' + edit: 'e Sửa bài viết' + delete: 'd Xóa bài viết' + mark_watching: 'm, w theo dõi chủ đề' + badges: + allow_title: "có thể sử dụng như là tiêu đề" + more_badges: + other: "+%{count} Thêm" + none: "" + badge_grouping: + getting_started: + name: Bắt đầu + community: + name: Cộng đồng + trust_level: + name: Độ tin cậy + other: + name: Khác + posting: + name: Đang đăng bài + badge: + editor: + name: Biên tập + description: Chỉnh sửa bàn viết lần đầu + basic_user: + name: Cơ bản + member: + name: Thành viên + regular: + name: Thường xuyên + leader: + name: Lãnh đạo + welcome: + name: Chào mừng + description: Đã nhận 1 lượt thích + autobiographer: + description: Filled user profile information + anniversary: + name: Ngày kỷ niệm + good_post: + name: Bài viết tốt + great_post: + name: Bài viết tuyệt vời + nice_topic: + name: Bài viết hay + good_topic: + name: Chủ đề tốt + nice_share: + description: Đã chia sẻ bài viết với 25 lượt người truy cập + good_share: + description: Đã chia sẻ bài viết với 300 lượt người truy cập + great_share: + description: Đã chia sẻ bài viết với 1000 lượt người truy cập + first_like: + name: Lượt thích đầu tiên + description: Đã thích một bài đăng + first_flag: + name: Đánh dấu đầu tiên + description: Đánh dấu bài viết + promoter: + description: Đã mời một thành viên + campaigner: + description: Mời 3 thành viên (Độ tin cậy 1) + champion: + description: Mời 5 thành viên (Độ tin cậy 2) + first_share: + name: Chia sẽ đầu tiên + description: Chia sẽ bài viết + first_link: + name: Liên kết đầu tiên + description: Thêm một liên kết từ chủ để khác + first_quote: + name: Trích dẫn đầu tiên + description: Trích dẫn thành viên + read_guidelines: + name: Xem hướng dẫn + reader: + name: Người xem + description: Đọc tất cả bài viết trong các chủ để có hơn 100 bài + popular_link: + name: Liên kết phổ biến + hot_link: + name: Liên kết hấp dẫn + famous_link: + name: Liên kết phổ biến + diff --git a/config/locales/server.vi_VN.yml b/config/locales/server.vi_VN.yml new file mode 100644 index 0000000000..cfea124c7c --- /dev/null +++ b/config/locales/server.vi_VN.yml @@ -0,0 +1,1021 @@ +vi_VN: + dates: + short_date_no_year: "D MMM" + short_date: "D MMM, YYYY" + long_date: "MMMM D, YYYY h:mma" + title: "Discourse" + topics: "Chủ đề" + posts: "bài viết" + loading: "Đang tải" + powered_by_html: 'Được hỗ trợ bởi Discourse, xem tốt nhất khi JavaScript được kích hoạt' + log_in: "Đăng nhập" + purge_reason: "Tự động xóa tài khoản không sử dụng, không kích hoạt." + disable_remote_images_download_reason: "Không thể tải ảnh về máy chủ vì thiếu dung lượng." + anonymous: "Ẩn danh" + errors: + format: '%{attribute} %{message}' + messages: + too_long_validation: "cho phép tối đa %{max} ký tự; bạn đã nhập %{length}." + invalid_boolean: "Giá trị boolean không hợp lệ" + taken: "đã được lấy trước" + accepted: phải được chấp nhận + blank: không thể để rỗng + present: phải để rỗng + confirmation: "%{attribute} không khớp" + empty: không thể để trống + equal_to: phải bằng %{count} + even: phải là chắn + exclusion: được bảo lưu + greater_than: phải lớn hơn %{count} + greater_than_or_equal_to: phải lớn hơn hoặc bằng %{count} + has_already_been_used: "đã được sử dụng" + inclusion: không được bao gồm trong danh sách + invalid: là không hợp lệ + is_invalid: "không hợp lệ; cố gắng cụ thể hơn một chút" + less_than: phải nhỏ hơn %{count} + less_than_or_equal_to: phải nhỏ hơn hoặc bằng %{count} + not_a_number: không phải là số + not_an_integer: phải là một số nguyên + odd: phải là số lẻ + record_invalid: 'Xác nhận thất bại: %{errors}' + restrict_dependent_destroy: + one: "Không thể xóa bản ghi bởi vì một bản ghi %{record} phụ thuộc đang tồn tại" + many: "Không thể xóa bản ghi bởi vì %{record} phụ thuộc tồn tại" + too_long: + other: quá dài (tối đa %{count} ký tự) + too_short: + other: quá ngắn (tối thiểu %{count} ký tự) + wrong_length: + other: độ dài không hợp lệ (nên đặt %{count} ký tự) + other_than: "phải khác %{count}" + template: + body: 'Đã có vấn đề với những trường sau:' + header: + other: '%{count} lỗi đã ngăn cản không thể lưu %{model} này' + embed: + load_from_remote: "Đã xảy ra lỗi khi tải bài viết." + site_settings: + min_username_length_exists: "Bạn không thể thiết lập chiều dài tối thiểu của username nhỏ hơn chiều dài của username ngắn nhất" + min_username_length_range: "Bạn không thiết lập giá trị nhỏ nhất lớn hơn giá trị lớn nhất" + max_username_length_exists: "Bạn không thể thiết lập chiều dài tối đa của username nhỏ hơn username dài nhất" + max_username_length_range: "Bạn không thể thiết lập số tối đa nhỏ hơn số tối thiểu" + default_categories_already_selected: "Bạn không thể chọn một danh mục được sử dụng trong danh sách khác." + s3_upload_bucket_is_required: "Bạn không thể tải lên S3 mà chưa thiết lập 's3_upload_bucket'." + bulk_invite: + file_should_be_csv: "Tập tin tải lên nên ở dạng csv hoặc txt." + backup: + operation_already_running: "Một tiến trình đang được thực hiện. Không thể bắt đầu một tiến trình mới ngay bây giờ." + backup_file_should_be_tar_gz: "Tập tin sao lưu nên nên ở dạng .tar.gz." + not_enough_space_on_disk: "Không đủ không gian trên đĩa để tải lên bản sao lưu này." + not_logged_in: "Bạn cần phải đăng nhập để thực hiện việc đó." + not_found: "Không thể tìm thấy đường dẫn hoặc tài nguyên yêu cầu." + invalid_access: "Bạn không được phép xem tài nguyên đã yêu cầu." + read_only_mode_enabled: "Trang web đang ở chế độ chỉ đọc. Tất cả các tương tác đã bị tắt." + too_many_replies: + other: "Xin lỗi bạn, người dùng mới tạm thời bị giới hạn với %{count} câu trả lời trong một chủ đề." + embed: + start_discussion: "Bắt đầu cuộc thảo luận" + continue: "Tiếp tục cuộc thảo luận" + more_replies: + other: "còn %{count} câu trả lời" + loading: "Đang tải cuộc thảo luận" + permalink: "Liên kết cố định" + imported_from: "Đây là cuộc thảo luận đi kèm chủ đề gốc tại %{link}" + in_reply_to: "▶ %{username}" + replies: + other: "%{count} câu trả lời" + no_mentions_allowed: "Xin lỗi, bạn không thể nhắc tới thành viên khác." + spamming_host: "Xin lỗi bạn không thể chèn liên kết tới trang đó." + user_is_suspended: "Người dùng đang bị treo không được phép đăng bài." + topic_not_found: "Có gì đó đã sai. Có lẽ chủ đề này đã bị đóng hoặc bị xóa trong khi bạn đang xem?" + just_posted_that: "rất giống với những gì bạn đã viết gần đây" + has_already_been_used: "đã được sử dụng" + invalid_characters: "chứa các kí tự không hợp lệ" + is_invalid: "không hợp lệ; cố gắng cụ thể hơn một chút" + next_page: "trang sau →" + prev_page: "← trang trước" + page_num: "Trang %{num}" + home_title: "Trang chủ" + topics_in_category: "Các chủ đề ở chuyên '%{category}'" + rss_posts_in_topic: "Nguồn cấp dữ liệu RSS của '%{topic}'" + rss_topics_in_category: "Nguồn cấp dữ liệu RSS của các chủ đề trong chuyên mục '%{category}'" + author_wrote: "%{author} đã viết:" + num_posts: "Bài đã đăng:" + num_participants: "Người tham gia:" + read_full_topic: "Đọc toàn bộ chủ đề" + private_message_abbrev: "Tin nhắn" + rss_description: + latest: "Chủ đề mới nhất" + hot: "Chủ đề nóng nhất" + posts: "Bài viết mới nhất" + too_late_to_edit: "Bài đăng đã được tạo từ rất lâu. Nó không thể được chỉnh sửa hoặc xóa nữa." + excerpt_image: "hình ảnh" + queue: + delete_reason: "Đã xóa thông qua hàng đợi kiểm duyệt" + groups: + errors: + can_not_modify_automatic: "Bạn không thể sửa đổi một nhóm tự động" + member_already_exist: "'%{username}' đã là thành viên của nhóm" + default_names: + everyone: "Mọi người" + admins: "quản trị" + moderators: "điều hành" + staff: "nhân viên" + trust_level_0: "trust_level_0" + trust_level_1: "trust_level_1" + trust_level_2: "trust_level_2" + trust_level_3: "trust_level_3" + trust_level_4: "trust_level_4" + education: + until_posts: + other: "%{count} bài đăng" + new-topic: | + Chào mừng bạn đến với %{site_name} — **cảm ơn vì đã đăng cuộc thảo luận mới!** + + - Bạn có cảm thấy tiêu đề có thú vị không khi bạn đọc to nó? Đó có phải là một đoạn tóm tắt tốt không? + + - Những ai sẽ hứng thú với cuộc thảo luận này? Tại sao đó lại trở thành vấn đề? Bạn muốn những loại phản hồi thế nào? + + - Sử dụng những từ khóa phổ biến để người khác có thể tìm thấy chủ đề của bạn dễ hơn. Để nhóm chủ đề của bạn với các chủ đề liên quan khác, hãy chọn chuyên mục cho chủ đề của mình. + + Xem thêm, [hướng dẫn cộng đồng của chúng tôi](/guidelines). Bảng điều khiển này sẽ chỉ xuất hiện vào lần đầu %{education_posts_text}. + new-reply: | + Chào mừng bạn đến với %{site_name} — **cám ơn vì đã đóng góp!** + + - Câu trả lời của bạn có làm cuộc thảo luận tốt hơn về mặt nào đó? + + - Hãy đối xử tốt với các thành viên khác trong cộng đồng của bạn. + + - Lời phê bình mang tính đóng góp cũng được chào đón, nhưng bạn nên phê bình *ý tưởng* chứ không phải con người. + + [Đọc hướng dẫn cộng đồng](/guidelines) để có thêm thông tin. Bảng này chỉ xuất hiện cho bài viết đầu tiên của bạn %{education_posts_text}. + avatar: | + ### How about a picture for your account? + + You've posted a few topics and replies, but your profile picture isn't as unique as you are -- it's just a letter. + + Have you considered **[visiting your user profile](%{profile_path})** and uploading a picture that represents you? + + It's easier to follow discussions and find interesting people in conversations when everyone has a unique profile picture! + sequential_replies: | + ### Xem xét việc trả lời nhiều bài viết cùng lúc + + Thay vì trả lời nhiều tuần tự đến từng chủ đề, xin vui lòng xem xét một bài trả lời duy nhất mà bao gồm các trích dẫn từ bài viết trước hoặc dùng tham chiếu @name. + + Bạn có thể sửa bài trả lời trước đó của bạn để thêm một trích dẫn bằng cách bôi đen và nhấn chọn nút quote reply vừa xuất hiện. + + Sẽ dễ dàng hơn cho tất cả mọi người để đọc chủ đề mà có ít câu trả lời sâu với nhiều cấp, trả lời cá nhân + dominating_topic: "### Hãy để người khác tham gia vào cuộc thảo luận\n\nChủ đề này rõ ràng là quan trọng với bạn & ndash; bạn đã đăng nhiều hơn% %{percent}% của các câu trả lời tại đây.\n\nBạn có chắc chắn bạn đang cung cấp đủ thời gian cho những người khác để chia sẻ quan điểm của mình? \n" + too_many_replies: | + ### Bạn đã đạt đến giới hạn trả lời cho chủ đề này + + Chúng tôi xin lỗi, nhưng người dùng mới bị giới hạn %{newuser_max_replies_per_topic} trả lời trong cùng một chủ đề. + + Thay vì thêm một câu trả lời khác, xin vui lòng xem xét chỉnh sửa trả lời trước đó của bạn, hoặc truy cập vào các chủ đề khác. + reviving_old_topic: "### Xem lại chủ đề này? \n\nCâu trả lời cuối cùng cho chủ đề này đã hơn hơn %{days} ngày. Trả lời của bạn sẽ đẩy chủ đề đó lên đầu danh sách và thông báo cho bất cứ ai liên quan đến cuộc thảo luận.\n\nBạn có chắc chắn bạn muốn tiếp tục cuộc trò chuyện cũ này? \n" + activerecord: + attributes: + category: + name: "Tên chuyên mục" + post: + raw: "Thân" + user_profile: + bio_raw: "GIới thiệu bản thân" + errors: + models: + topic: + attributes: + base: + warning_requires_pm: "Bạn chỉ có thể đính kèm cảnh báo qua tin nhắn cá nhân" + too_many_users: "Bạn chỉ có thể gửi một cảnh báo tới một người dùng mỗi lần." + cant_send_pm: "Xin lỗi, bạn không thể gửi tin nhắn tới thành viên này" + no_user_selected: "Bạn phải chọn một thành viên phù hợp." + user: + attributes: + password: + common: "là một trong 10000 mật khẩu được sử dụng nhiều nhất. Vui lòng sử dụng một mật khẩu an toàn hơn." + same_as_username: "giống với tên đăng nhập của bạn. Vui lòng sử dụng mật khẩu bảo mật hơn." + same_as_email: "giống với email của bạn. Vui lòng sử dụng mật khẩu bảo mật hơn" + ip_address: + signup_not_allowed: "Đăng ký không cho phép tài khoản này" + color_scheme_color: + attributes: + hex: + invalid: "không phải là một màu không hợp lệ" + user_profile: + no_info_me: "
Mục nói về bản thân bạn trong hồ sơ của bạn hiện đang trống, bạn muốn điền vào nó? " + no_info_other: "
%{name} đã không nhập bất cứ điều gì nói về bản thân của họ " + vip_category_name: "Phòng khách" + vip_category_description: "Một chuyên mục chỉ dành cho thành viên có mức tin tưởng 3 hoặc cao hơn" + meta_category_name: "Phản hồi" + meta_category_description: "Thảo luận về site này, tổ chức của nó, làm sao nó hoạt động, và làm sao chúng tôi có thể cải tiến nó tốt hơn." + staff_category_name: "Nhân viên" + staff_category_description: "Chuyên mục riêng dành cho nhân viên. Các chủ đề chỉ hiển thị với quản trị viên và điều hành viên." + assets_topic_body: "Chủ đề này tồn tại vĩnh viễn, chỉ xem được bởi nhân viên, dùng để chứa ảnh và file dành cho việc thiết kế. Vui lòng đừng xóa.\n\n\nHướng dẫn:\n\n\n1. Trả lời chủ đề này.\n2. Tải lên tất cả ảnh bạn cần cho logo, favicon, và các thứ khác. (Dùng nút tải lên trong công cụ viết bài, hoặc kéo-và-thả hoặc dán ảnh vào) \n3. Gửi trả lời của bạn.\n4. Bấm chuột phải lên ảnh trong bài đăng mới để chép đường dẫn của các ảnh đã được tải lên, hoặc sửa bài viết của bạn để lấy đường dẫn của ảnh. Sao chép các đường dẫn này.\n5. Dán các đường dẫn này vào [thiết lập chung](/admin/site_settings/category/required).\n\n\nNếu bạn cần tải lên các loại file khác, sửa `authorized_extensions` trong [thiết lập file](/admin/site_settings/category/files)." + lounge_welcome: + title: "Chào mừng bạn đến với Phòng khách" + body: |2 + + Chúc mừng! :confetti_ball: + + Nếu bạn có thể xem chủ đề này, bạn đã được thăng lên bậc **thường xuyên** (bậc tin tưởng 3). + + Bạn có thể … + + * Sửa tiêu đề của bất kì chủ đề nào + * Sửa chuyên mục của bất kì chủ đề nào + * Tất cả liên kết ở trạng thái follow ([những liên kết nofollow](http://en.wikipedia.org/wiki/Nofollow) sẽ được loại bỏ) + * Truy cập vào phòng khách dành riêng cho thành viên với bậc tin tưởng 3 hoặc cao hơn + * Ẩn bài viết spam với 1 lần đánh dấu. + + Đây là danh sách [của các thành viên thường xuyên](/badges/3/regular). Hãy chào họ đi nào. + + Cảm ơn vì đã trở thành một phần không thể thiếu đối với cộng đồng. + + (Để biết thêm chi tiết về bậc tin tưởng, [xem chủ đề này][trust]. Hãy nhớ rằng bạn phải tiếp tục đạt được các yêu cầu để duy trì bậc tin tưởng của mình.) + + [trust]: https://meta.discourse.org/t/what-do-user-trust-levels-do/4924 + category: + topic_prefix: "Giới thiệu chuyên mục %{category}" + errors: + uncategorized_parent: "Mục \"Chưa được phân loại\" không thể có một chuyên mục chính" + self_parent: "Cha của chủ đề phụ không thể nào là chính nó" + depth: "Bạn không thể để một chuyên mục con trong một chuyên mục con khác." + email_in_already_exist: "Địa chỉ thư đến '%{email_in}' đã được sử dụng cho danh mục '%{category_name}'" + cannot_delete: + uncategorized: "Không thể xoá mục Chưa phân loại" + has_subcategories: "Không thể xoá chuyên mục này được vì nó có chuyên mục con." + topic_exists: + other: "Không thể xoá phân loại này được bởi vì nó có %{count} chủ đề. Các chủ đề cũ là %{topic_link}." + topic_exists_no_oldest: "Không thể xoá chuyên mục này vì nó có %{count} chủ để." + trust_levels: + newuser: + title: "thành viên mới" + basic: + title: "thành viên cơ bản" + change_failed_explanation: "Bạn đã cố gắng để giảm hạng %{user_name} xuống '%{new_trust_level}'. Tuy nhiên cấp độ tin cậy hiện tại của họ đã là '%{current_trust_level}'. %{user_name} sẽ được giữ lại ở cấp độ '%{current_trust_level}' - nếu bạn muốn giảm hạng thành viên, trước tiên hãy khóa cấp độ tin cậy" + rate_limiter: + too_many_requests: "Hành động bạn vừa thực hiện bị giới hạn theo ngày. Hãy chờ %{time_left} và thử lại." + hours: + other: "%{count} giờ" + minutes: + other: "%{count} phút" + seconds: + other: "%{count} giây" + datetime: + distance_in_words: + half_a_minute: "< 1 phút" + less_than_x_seconds: + other: "< %{count} giây" + x_seconds: + other: "%{count} giây" + less_than_x_minutes: + other: "< %{count} phút" + x_minutes: + other: "%{count} phút" + about_x_hours: + other: "%{count} giờ" + x_days: + other: "%{count} ngày" + about_x_months: + other: "%{count} tháng" + x_months: + other: "%{count} tháng" + about_x_years: + other: "%{count} năm" + over_x_years: + other: "> %{count} năm" + almost_x_years: + other: "%{count} năm" + distance_in_words_verbose: + half_a_minute: "ngay bây giờ" + less_than_x_seconds: + other: "ngay bây giờ" + x_seconds: + other: "%{count} giây trước" + less_than_x_minutes: + other: "ít hơn %{count} phút trước" + x_minutes: + other: "%{count} phút trước" + about_x_hours: + other: "%{count} giờ trước" + x_days: + other: "%{count} ngày trước" + about_x_months: + other: "khoảng %{count} tháng trước" + x_months: + other: " %{count} tháng trước" + about_x_years: + other: "khoảng %{count} năm trước" + over_x_years: + other: "hơn %{count} năm trước" + almost_x_years: + other: "gần %{count} năm trước" + password_reset: + no_token: "Xin lỗi, liên kết đổi mật khẩu đã cũ. Chọn \"Đăng nhập\" và sử dụng chức năng \"Quên mật khẩu\" để lấy liên kết mới." + choose_new: "Vui lòng chọn mật khẩu mới" + choose: "Bạn phải nhập mật khẩu" + update: 'Cập nhật mật khẩu' + save: 'Nhập mật khẩu' + title: 'Thiết lập lại mật khẩu' + success: "Bạn đã thay đổi mật khẩu thành công và đã được đăng nhập." + success_unapproved: "Bạn đã thay đổi mật khẩu thành công." + continue: "Tiếp tục đến %{site_name}" + change_email: + confirmed: "Email của bạn đã được cập nhật." + please_continue: "Tiếp tục đến %{site_name}" + error: "Có một lỗi khi thay đổi địa chỉ email của bạn. Có lẽ email này đã được sử dụng rồi?" + activation: + action: "Nhấn vào đây để kích hoạt tài khoản của bạn" + already_done: "Xin lỗi, liên kết để xác nhận tài khoản này không còn hợp lệ. Có thể tài khoản của bạn được kích hoạt?" + please_continue: "Tài khoản của bạn đã được xác nhận; bạn sẽ được chuyển đến trang chủ." + continue_button: "Tiếp tục tới %{site_name}" + welcome_to: "Chào mừng bạn đến với %{site_name}!" + approval_required: "Một điều hành viên phải duyệt tài khoản của bạn trước khi bạn có thể đăng nhập diễn đàn này. Bạn sẽ nhận được email khi tài khoản của bạn được duyệt!" + missing_session: "Chúng tôi không thể xác" + post_action_types: + off_topic: + title: 'Không-đúng-chủ-đề' + description: 'Bài này không liên quan đến các cuộc thảo luận hiện nay theo quy định của các tiêu đề và bài đầu tiên, và có lẽ nó nên được di chuyển đến những nơi khác.' + long_form: 'đánh dấu không-đúng-chủ-đề' + spam: + title: 'Spam' + description: 'Bài đăng này là một bài quảng cáo. Không bổ ích hoặc liên quan tới chủ đề hiện tại, chỉ nhằm mục đích quảng cáo.' + long_form: 'đánh dấu là spam' + email_title: '"%{title}" đã bị gắn cờ spam' + email_body: "%{link}\n\n%{message}" + inappropriate: + title: 'Không thích hợp' + description: 'Chủ để này chứa nội dung mà bình thường được xem là xúc phạm, lạm dụng, hoặc vi phạm nguyên tắc cộng đồng.' + long_form: 'đánh dấu cái này không thích hợp' + notify_user: + long_form: 'đã nhắn tin cho thành viên' + email_title: 'Bài đăng của bạn trong "%{title}"' + email_body: "%{link}\n\n%{message}" + notify_moderators: + title: "Một thứ khác" + email_body: "%{link}\n\n %{message}" + bookmark: + title: "Đánh dấu chỉ mục \x1C" + description: 'Đánh dấu chỉ mục bài viết này' + long_form: 'đã đánh dấu chỉ mục bài viết này' + like: + title: 'Thích' + description: 'Thích bài viết này' + long_form: 'đã thích cái này' + vote: + title: 'Bầu chọn' + description: 'Bầu cho bài viết này' + long_form: 'bầu cho bài viết này' + topic_flag_types: + spam: + title: 'Rác' + description: 'Chủ đề này mang bản chất quảng cáo, không có ích và không thích hợp với nơi này.' + long_form: 'đã đánh dấu bài này dạng bài viết rác' + inappropriate: + title: 'Không phù hợp' + description: 'Chủ để này chứa nội dung mà với lý lẽ thường nhật được xem là xúc phạm, lạm dụng, hoặc vi phạm chỉ dẫn chung của cộng đồng.' + long_form: 'đã đánh dấu bài này không phù hợp' + notify_moderators: + title: "Một cái khác" + long_form: 'đã đánh dấu cho điều hành viên xem xét' + email_title: 'Chủ đề "%{title}" cần được ban điều hành quan tâm' + email_body: "%{link}\n\n%{message}" + flagging: + you_must_edit: '

Bài viết của bạn đã được gắn cờ bở cộng đồng. Vui lòngxem tin nhắn của bạn.

' + user_must_edit: '

Bài viết này đã bị đánh dấu bởi cộng đồng và đang được ẩn tạm thời.

' + archetypes: + regular: + title: "Chủ đề thường" + banner: + title: "Banner chủ đề" + message: + make: "Chủ đề này trở thành một banner. Nó sẽ hiện ở đầu mọi trang tới khi nó được tắt bởi thành viên." + remove: "Chủ đề này không còn là một banner. Nó sẽ không hiện ở đầu mọi trang nữa." + unsubscribed: + title: 'Hủy bỏ đăng ký' + description: "Bạn đã ngừng đăng. Chúng tôi sẽ không liên lạc bạn nữa!" + oops: "Trong trường hợp bạn không có ý thực hiện thao tác này, bấm vào bên dưới." + error: "Lỗi hủy đăng kí" + preferences_link: "Bạn có thể hủy theo dõi bản tin tóm tắt tại trang thiết lập" + different_user_description: "Bạn đang đăng nhập như một người dùng khác, không phải là người dùng đã được gửi đến qua mail. Hãy thoát ra và thử lại." + not_found_description: "Xin lỗi, chúng tôi không thể ngừng đăng ký bạn. Có thể là do link trong email của bạn đã hết hạn." + resubscribe: + action: "Đăng ký lại" + title: "Đã đăng ký lại!" + description: "Bạn đã được đăng ký lại." + reports: + visits: + title: "Các thành viên truy cập" + xaxis: "Ngày" + yaxis: "Số lần truy cập" + signups: + title: "Thành viên mới" + xaxis: "Ngày" + yaxis: "Số lượng thành viên mới" + profile_views: + title: "Xem hồ sơ người dùng" + xaxis: "Ngày" + yaxis: "Số người đã xem hồ sơ người dùng" + topics: + title: "Các chủ đề" + xaxis: "Ngày" + yaxis: "Số lượng chủ đề mới" + posts: + title: "Bài viết" + xaxis: "Ngày" + yaxis: "Số lượng bài viết mới" + likes: + title: "Lượt thích" + xaxis: "Ngày" + yaxis: "Số lượt thích mới" + flags: + title: "Dấu cờ - Flags" + xaxis: "Ngày" + yaxis: "Số dấu cờ - flag" + bookmarks: + title: "Các đánh dấu" + xaxis: "Ngày" + yaxis: "Số đánh dấu mới" + starred: + title: "Bắt đầu" + xaxis: "Ngày" + yaxis: "Số chủ đề được tạo." + users_by_trust_level: + title: "Thành viên ở mõi bậc tin tưởng" + xaxis: "Bậc tin tưởng" + yaxis: "Số thành viên" + emails: + title: "Email đã gửi" + xaxis: "Ngày" + yaxis: "Số lượng emails" + user_to_user_private_messages: + title: "Người dùng tới người dùng" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + system_private_messages: + title: "Hệ thống" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + moderator_warning_private_messages: + title: "Cảnh báo của điều hành viên" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + notify_moderators_private_messages: + title: "Thông báo ban quản trị" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + notify_user_private_messages: + title: "Thông báo người dùng" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + top_referrers: + title: "Giới thiệu hàng đầu" + xaxis: "Người dùng" + num_clicks: "Clicks" + num_topics: "Chủ đề" + top_traffic_sources: + title: "Nguồn truy cập" + xaxis: "Tên miền" + num_clicks: "Clicks" + num_topics: "Chủ đề" + num_users: "Người dùng" + top_referred_topics: + title: "Top chủ đề giới thiệu" + xaxis: "Chủ đề" + num_clicks: "Clicks" + page_view_anon_reqs: + title: "Ẩn danh" + xaxis: "Ngày" + yaxis: "Truy cập API ẩn danh" + page_view_logged_in_reqs: + title: "Đã đăng nhập" + xaxis: "Ngày" + yaxis: "Đã đăng nhập truy cập API" + page_view_crawler_reqs: + title: "Thu thập thông tin web" + xaxis: "Ngày" + yaxis: "Truy cập API thu thập thông tin web" + page_view_total_reqs: + title: "Tổng số" + xaxis: "Ngày" + yaxis: "Tổng số truy cập API" + page_view_logged_in_mobile_reqs: + title: "Trong yêu cầu đăng nhập API" + xaxis: "Ngày" + yaxis: "Mobile yêu cầu Đăng nhập API" + page_view_anon_mobile_reqs: + title: "Anon API Requests" + xaxis: "Ngày" + yaxis: "Mobile Anon API Requests" + http_background_reqs: + title: "Hình nền" + xaxis: "Ngày" + yaxis: "Yêu cầu đã sử dụng cho cập nhật thời gian thực và thống kê" + http_2xx_reqs: + title: "Trạng thái 2xx (OK)" + xaxis: "Ngày" + yaxis: "Yêu cầu thành công (Trạng thái 2xx)" + http_3xx_reqs: + title: "HTTP 3xx (Chuyển hướng)" + xaxis: "Ngày" + yaxis: "Chuyển hướng yêu cầu (Trạng thái 3xx)" + http_4xx_reqs: + title: "HTTP 4xx (Trình khách lỗi)" + xaxis: "Ngày" + yaxis: "Trình khách lỗi (Trạng thái 4xx)" + http_5xx_reqs: + title: "HTTP 5xx (Máy chủ lỗi)" + xaxis: "Ngày" + yaxis: "Máy chủ lỗi (Trạng thái 5xx)" + http_total_reqs: + title: "Tổng số" + xaxis: "Ngày" + yaxis: "Tổng số yêu cầu" + time_to_first_response: + title: "Thời gian để phản hồi lần đầu" + xaxis: "Ngày" + yaxis: "Thời gian trung bình (giờ)" + topics_with_no_response: + title: "Chủ đề không có phản hồi" + xaxis: "Ngày" + yaxis: "Tổng số" + mobile_visits: + title: "Các thành viên truy cập" + xaxis: "Ngày" + yaxis: "Số lần truy cập" + dashboard: + rails_env_warning: "Máy chủ của bạn đang chạy trong chế độ %{env}." + ruby_version_warning: "Bạn đang dùng một phiên bản Ruby 2.0.0 được biết là có nhiều vấn đề. Hãy nâng cấp bản vá 247 hoặc mới hơn." + host_names_warning: "Cài đặt của bạn config/database.yml đang sử dụng hostname mặc định. Cập nhật lại để sử dụng hostname của bạn" + gc_warning: 'Máy chủ của bạn hiện tại sử dụng cơ chế dọn rác mặc định của ruby, điều này khiến cho hiệu năng của máy chủ không tốt lắm. Đọc chủ đề sau cho việc tối ưu hiệu năng Tối ưu Ruby and Rails cho Discourse.' + sidekiq_warning: ' Sidekiq đang không hoạt động. Rất nhiều tác vụ, như gửi email, là được thực thi không đồng bộ bởi sidekiq. Hãy chắc chắn rằng ít nhất một tiến trình sidekiq phải đang hoạt động. Đọc thêm về Sidekiq tại đây.' + memory_warning: 'Máy chủ của bạn có bộ nhớ ít hơn 1 GB. Khuyến cáo sử dụng bộ nhớ tối thiểu 1 GB .' + google_oauth2_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Google OAuth2 (enable_google_oauth2_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' + facebook_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Facebook (enable_facebook_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' + twitter_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Twitter (enable_twitter_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' + github_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với GitHub (enable_github_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' + s3_config_warning: 'Máy chủ được cấu hình để upload file lên s3, tuy nhiên ít nhất một trong các tùy chỉnh sau đây không được thiết lập: s3_access_key_id, s3_secret_access_key hoặc s3_upload_bucket. Truy cập Thiết lập Site và bổ sung các thiết lập đó. Xem bài viết "How to set up image uploads to S3?" để biết thêm chi tiết.' + s3_backup_config_warning: 'Máy chủ được cấu hình để upload các bản sao lưu dữ liệu lên s3, tuy nhiên ít nhất một trong các tùy chỉnh sau đây không được thiết lập: s3_access_key_id, s3_secret_access_key hoặc s3_backup_bucket. Truy cập Thiết lập Site và bổ sung các thiết lập đó. Xem bài viết "How to set up image uploads to S3?" để biết thêm chi tiết.' + image_magick_warning: 'Máy chủ đã cấu hình để tạo hình đại diện nhỏ từ những hình lới, nhưng ImageMagick chưa được cài đặt. Cài ImageMagick sử dụng trình quản lý package yêu thích của bạn hoặc tải về phiên bản mới nhất.' + failing_emails_warning: 'Có %{num_failed_jobs} email jobs thấ bại. Kiểm tra app.yml và chắc chắn rằng cấu hình máy chủ email đúng. Xem jobs thất bại ở Sidekiq.' + default_logo_warning: "Cập nhập logo của trang. Cập nhập logo_url, logo_small_url, và favicon_url trong Thiết lập trang." + contact_email_invalid: "Email liên lạc của trang không hợp lệ. Cập nhật trong Thiết lập trang/a>." + title_nag: "Nhập tên trang của bạn. Cập nhập tiêu đề trong Thiết lập trang." + consumer_email_warning: "Trang web của bạn được cài đặt sử dụng Gmail (hoặc một dịch vụ email khác) để gửi email. Gmail có giới hạn số lượng email bạn có thể gửi. Hãy xem xét sử dụng một dịch vụ email khác như mandrill.com để đảm bảo khả năng vận chuyển tất cả các email." + site_settings: + censored_words: "Từ sẽ tự động thay thế bằng ■■■■" + delete_old_hidden_posts: "Tự động ẩn bất kỳ bài viết ở ẩn hơn 30 ngày." + default_locale: "Ngôn ngữ mặc định của Discourse (Mã ISO 639-1)" + allow_user_locale: "Cho phép thành viên chọn ngôn ngữ của riêng trong thiết lập giao diện." + min_post_length: "Số kí tự tối thiểu trong bài đăng." + min_first_post_length: "Chiều dài tối thiểu cho bài viết đầu tiên (nội dung chủ đề) tính theo ký tự." + min_private_message_post_length: "Số kí tự tối thiểu trong tin nhắn." + max_post_length: "Số kí tự tối đa trong bài đăng." + min_topic_title_length: "Số kí tự tối thiểu trong tiêu đề chủ đề." + max_topic_title_length: "Số kí tự tối đa trong tiêu đề chủ đề." + min_private_message_title_length: "Chiều dài tối thiểu cho phép theo số kí tự của một thông điệp" + min_search_term_length: "Số kí tự tối thiểu trong từ khóa tìm kiếm." + uncategorized_description: "Mô tả của chuyên mục \"Không phân loại\". Để trống khi không muốn mô tả." + allow_duplicate_topic_titles: "Cho phép các chủ đề trùng tiêu đề." + unique_posts_mins: "Trong bao nhiêu phút người sử dụng có thể viết bài khác với nội dung giống nhau" + title: "Tên của trang này, sử dụng trong thẻ tiêu đề" + site_description: "Mô tả trang này trong một câu, nó sẽ được sử dụng trong thẻ meta description" + contact_email: "Địa chỉ email liên hệ của người chịu trách nhiệm trang này. Sử dụng cho những thông báo quan trọng giống như cờ không được quản lý, cũng giống form liện hệ /about cho những vấn đề cấp bách." + contact_url: "URL liên hệ trong trang này. Sử dụng trong form liên hệ /about cho những vấn đề cấp bách." + queue_jobs: "DEVELOPER ONLY! WARNING! By default, queue jobs in sidekiq. If disabled, your site will be broken." + crawl_images: "Lấy hình ảnh tử URL bên ngoài để thêm vào đúng chiều dài và chiều cao." + download_remote_images_to_local: "Tải ảnh về lưu trữ để tránh ảnh bị hư." + download_remote_images_threshold: "Dung lượng tối thiểu cần để tải ảnh từ xa về lưu trữ (tính bằng phần trăm)" + disabled_image_download_domains: "Tải ảnh từ xa sẽ không áp dụng với các tên miền sau. Phân cách bằng dấu |" + post_edit_time_limit: "Tác giả có thể sửa hoặc xóa bài viết của họ trong (n) phút sau khi đăng. 0 là mãi mãi." + edit_history_visible_to_public: "Cho phép mọi người nhìn thấy phiên bản trước khi chỉnh sửa bài viết. Khi không cho phép, chỉ nhân viên có thể xem." + delete_removed_posts_after: "Bài viết đã được xóa bởi tác giả sẽ được tự động xóa sau (n) giờ. Nếu cài là 0, bài viết sẽ được xóa ngay lập tức." + max_image_width: "Chiều rộng tối đa của ảnh thu nhỏ trong bài viết." + max_image_height: "Chiều cao tối đa của ảnh thu nhỏ trong bài viết." + category_featured_topics: "Số chủ đề hiện thị mỗi danh mục trong trang /categories. Sau khi thay đổi giá trị này, nó sẽ mất khoảng 15 phút để trang danh mục cập nhật." + show_subcategory_list: "Hiện danh sách chuyên mục con thay vì danh sách chủ đề khi truy cập vào chuyên mục." + fixed_category_positions: "Nếu được bật, bạn sẽ có thể sắp xếp chuyên mục theo một thứ tự cố định. Nếu không bật, chuyên mục sẽ được sắp xếp theo thứ tử hoạt động." + fixed_category_positions_on_create: "Nếu chọn, sắp xếp danh mục sẽ được thực hiện trong cửa sổ tạo chủ đề (yêu cầu fixed_category_positions)." + post_excerpt_maxlength: "Chiều dài tối đa của đoạn trích / tóm tắt chủ đề." + favicon_url: "Favicon cho trang của bạn, xem tại http://en.wikipedia.org/wiki/Favicon, để chạy được với CDN ảnh phải là png" + mobile_logo_url: "Cố định vị trí hình logo sử dụng tại phía trên bên trái trang mobile của bạn. Nên là hình vuông. Nếu để trống, sẽ sử dụng `logo_url`. Ví dụ: http://example.com/uploads/default/logo.png" + apple_touch_icon_url: "Biểu tượng sử dụng trong các thiết bị cảm ứng của Apple. Kích thước gợi ý 144px x 144px" + email_custom_headers: "Danh sách xác định email header tùy chỉnh" + email_subject: "Tùy biến định dạng chủ đề cho chuẩn email. Xem tại https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" + use_https: "URL đầy đủ đến trang (Discourse.base_url) là http hoặc https? KHÔNG BẬT NÓ CHO TỚI KHI HTTPS ĐÃ CÀI ĐẶT SẴN SẰNG VÀ ĐÃ CHẠY!" + summary_score_threshold: "Số điểm tối thiểu yêu cầu cho một bài viết bao gồm 'Tóm tắt chủ đề này'" + summary_posts_required: "Số bài viết tối thiểu trong một chủ đề trước khi 'Tóm tắt chủ đề này' được kích hoạt" + summary_likes_required: "Số lượt thích trong một chủ đề trước khi 'Tóm tắt chủ đề này' được kích hoạt" + summary_percent_filter: "Khi người dùng nhấn 'Tóm tắt chủ đề này', hiển thị phí trên % của bài viết" + summary_max_results: "Số bài viết tối đa trả ra bởi 'Tóm tắt chủ đề này'" + cooldown_minutes_after_hiding_posts: "Số phút một người dùng phải chờ trước khi họ có thể sửa một bài viết ẩn bởi gắn cờ cộng đồng" + max_topics_in_first_day: "Số chủ đề tối đa một thành viên được tạo trong ngày đầu tiên." + max_replies_in_first_day: "Số trả lời tối đa một thành viên được tạo trong ngày đầu tiên" + tl2_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 2 (thành viên) bằng cách nhân với số này" + tl3_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 3 (bình thường) bằng cách nhân với số này" + tl4_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 4 (dẫn đầu) bằng cách nhân với số này" + ga_tracking_code: "Mã theo dõi Google analytics (ga.js), ví dụu: UA-12345678-9; chi tiết http://google.com/analytics" + ga_domain_name: "Tên miền Google analytics (ga.js), ví dụ: mysite.com; chi tiết http://google.com/analytics" + ga_universal_tracking_code: "Mã theo dõi Google Universal Analytics (analytics.js) , Ví dụ: UA-12345678-9; chi tiết http://google.com/analytics" + ga_universal_domain_name: "Tên miền Google Universal Analytics (analytics.js), ví dụ: mysite.com; chi tiết http://google.com/analytics" + enable_escaped_fragments: "Trả lại tới Google's Ajax-Crawling API nếu không xác định được webcrawler. Xem chi tiết https://support.google.com/webmasters/answer/174992?hl=en" + enable_noscript_support: "Cho phép webcrawler search engine chuẩn hỗ trợ bằng thẻ noscript" + allow_moderators_to_create_categories: "Cho phép điều hành viên tạo danh mục mới" + email_token_valid_hours: "Token quyên mật khẩu / kích hoạt tài khoản có giá trị trong (n) giờ." + email_token_grace_period_hours: "Token quyên mật khẩu / kích hoạt tài khoản vẫn còn giá trị (n) giờ sau khi được gia hạn" + enable_badges: "Kích hoạt hệ thống huy hiệu" + allow_index_in_robots_txt: "Chỉ rõ trong robots.txt trang web này cho phép tạo chỉ mục bởi web search engines." + email_domains_blacklist: "Một danh sách đuôi email mà người dùng không được phép dùng để đăng ký tài khoản. Ví dụ: maillinator.com|trashmail.net. Lưu ý mỗi tên miền cách nhau bởi dấu \"|\"." + email_domains_whitelist: "Danh sách tên miền người dùng ĐƯỢC PHÉP đăng ký tài khoản. CẢNH BÁO: người dùng với tên miền email khác trong danh sách sẽ không được phép đăng ký!" + forgot_password_strict: "Không thông báo cho người dùng tài không tồn tại khi họ dùng chức năng quyên mật khẩu." + log_out_strict: "Khi đăng xuất, đăng xuất TẤT CẢ session cho tất cả thiế bị" + version_checks: "Ping Discourse Hub để cập nhật phiên bản và hiện thông báo phiên bản mới trong bảng điều khiển quản trị" + new_version_emails: "Gửi email đến địa chỉ contact_email khi có phiên bản Discourse mới." + port: "DEVELOPER ONLY! WARNING! Sử dụng HTTP port thay vì mặc định port 80. Để trống mặc định port 80." + force_hostname: "DEVELOPER ONLY! LƯU Ý! Chỉ rõ hostname trong URL. Để trống là mặc định." + invite_expiry_days: "Key mời người dùng có giới hạn bao lâu? tính theo ngày" + invite_only: "Đăng ký tự do đã khóa, tất cả người dùng phải được mời bởi những thành viên khác hoặc nhân viên." + login_required: "Yêu cầu chứng thực để đọc nội dung trên trang web, không cho phép người dùng nặc danh truy cập." + min_username_length: "Chiều dài username tối thiểu." + max_username_length: "Chiều dài username tối đa." + reserved_usernames: "Những username không được phép đăng ký." + min_password_length: "Chiều dài mật khẩu tối thiểu." + block_common_passwords: "Không cho phép mật khẩu trong danh sách 10.000 mật khẩu phổ biến." + enable_sso: "Cho phép dùng single sign on bằng trang ngoài (CẢNH BÁO: ĐỊA CHỈ EMAIL CỦA NGƯỜI DÙNG PHẢI ĐƯỢC CHỨNG THỰC BỞI TRANG NGOÀI!)" + sso_url: "URL của single sign on enpoint" + sso_secret: "Chuỗi bảo mật đã được sử dụng để chứng thực thông tin SSO, chắc chắn nó có ít nhất 10 ký tự." + sso_not_approved_url: "Chuyển những tài khoản SSO chưa duyệt tới URL này" + allow_new_registrations: "Cho phép đăng ký người dùng mới. Bỏ chọn để bất cứ ai cũng có thể tạo tài khoản mới." + enable_yahoo_logins: "Cho phé chứng thực qua Yahoo" + enable_google_oauth2_logins: "Cho phép chứng thực qua Google Oauth2. Nó là cách chứng thực mà Google hỗ trợ. Yêu cầu key và secret." + google_oauth2_client_id: "Client ID ứng dụng Google của bạn." + google_oauth2_client_secret: "Client secret ứng dụng Google của bạn." + enable_twitter_logins: "Cho phép chứng thực qua Twitter, yêu cầu twitter_consumer_key và twitter_consumser_secret" + twitter_consumer_key: "Consumer key cho chứng thực Twitter, đăng ký tại http://dev.twitter.com" + twitter_consumer_secret: "Consumer secret cho chứng thực Twitter, đăng ký tại http://dev.twitter.com" + enable_facebook_logins: "Cho phép chứng thực Facebook, yêu cầu facebook_app_id và facebook_app_secret" + facebook_app_id: "App id cho chứng thực Facebook, đăng ký tại https://developers.facebook.com/apps" + facebook_app_secret: "App secret cho chứng thực Facebook, đăng ký tại https://developers.facebook.com/apps" + enable_github_logins: "Cho phép chứng thực Github, yêu cầu gitbug_client_id và githup_client_secret" + github_client_id: "Client id cho chứng thực Github, đăng ký tại https://github.com/settings/applications" + github_client_secret: "Client secret cho chứng thực Github, đăng ký tại https://github.com/settings/applications" + allow_restore: "Cho phép phục hồi, nó có thể thay thế TẤT CẢ dữ liệu trang web! Bỏ chọn, trừ khi bạn có kế hoạch phục hồi một bản sao lưu" + maximum_backups: "Số bản sao lưu tối đa lưu trong đĩa cứng. Những bản sao lưu cũ sẽ được xóa tự động" + automatic_backups_enabled: "Chạy sao lưu tự động như cấu hình trong tần số sao lưu" + backup_frequency: "Tần số sao lưu trang web, trong ngày." + enable_s3_backups: "Tải bản sao lưu lên S3 khi hoàn tất. QUAN TRỌNG: yêu cầu chứng thực S3 đã được nhập trong cấu hình File." + active_user_rate_limit_secs: "Tần số cập nhật trường 'last_seen_at, tính theo giây" + rate_limit_create_topic: "Sau khi tạo một chủ đề, người dùng phải chờ (n) giây trước khi tạo một chủ đề khác." + rate_limit_create_post: "Sau khi đăn bài, người dùng phải chờ (n) giây trước khi đăng bài khác." + rate_limit_new_user_create_topic: "Sau khi tạo một chủ đề, người dùng mới phải chờ (n) giây trước khi tạo chủ đề khác." + rate_limit_new_user_create_post: "Sau khi đăng bài, người dùng mới phải chờ (n) giây trước khi đăng bài khác." + max_likes_per_day: "Số tối đa người dùng có thể like mỗi ngày." + max_flags_per_day: "Số tối đa mà người dùng có thể gắn cờ mỗi ngày." + max_bookmarks_per_day: "Số tối đa người dùng có thể đánh dấu mỗi ngày." + max_edits_per_day: "Số tối đa người dùng có thể chỉnh sửa mỗi ngày." + max_topics_per_day: "Số chủ đề tối đa người dùng có thể tạo mỗi ngày." + max_private_messages_per_day: "Số tin nhắn tối đa người dùng có thể tạo mỗi ngày." + max_invites_per_day: "Số tối đa người dùng có thể gửi lời mời mỗi ngày." + suggested_topics: "Số chủ đề gợi ý hiện ở cuối một chủ đề" + limit_suggested_to_category: "Chỉ hiện thị những chủ đề từ danh mục hiện tại trong chủ đề gợi ý." + s3_access_key_id: "Amazon S3 access key id này sẽ được sử dụng để tải lên ảnh." + s3_secret_access_key: "Amazon S3 secret access key này sẽ được sử dụng để tải lên ảnh." + s3_region: "Amazon S3 region name sẽ được sử dụng để tải lên ảnh." + avatar_sizes: "Danh sách những kích thước hình đại diện tự động khởi tạo." + external_system_avatars_enabled: "Sử dụng dịch vụ ảnh đại diện bên ngoài." + default_invitee_trust_level: "Bậc tin tưởng mặc định (0-4) cho thành viên được mời." + tl1_requires_topics_entered: "Số chủ đề một thành viên mới phải truy cập trước khi được lên bậc tin tưởng 1" + tl1_requires_read_posts: "Số chủ đề một thành viên mới phải đọc trước khi được lên bậc tin tưởng 1" + tl1_requires_time_spent_mins: "Số phút một thành viên mới phải đọc trước khi được lên bậc tin tưởng 1" + tl2_requires_topics_entered: "Số chủ đề một thành viên mới phải truy cập trước khi được lên bậc tin tưởng 2" + tl2_requires_read_posts: "Số chủ đề một thành viên mới phải đọc trước khi được lên bậc tin tưởng 2" + tl2_requires_time_spent_mins: "Số phút một thành viên mới phải đọc trước khi được lên bậc tin tưởng 2" + min_trust_to_create_topic: "Bậc tin tưởng tối thiểu để tạo một chủ đề mới." + newuser_max_links: "Bao nhiêu liên kết tài khoản mới có thể thêm vào bài viết." + newuser_max_images: "Bao nhiêu hình tài khoản mới có thể thêm vào bài viết." + newuser_max_attachments: "Bao nhiêu đính kèm tài khoản mới có thể thêm vào bài viết" + email_time_window_mins: "Chờ (n) phút trước khi gửi bất kỳ một email thông báo nào, để cung cấp cho người dùng cơ hội để chỉnh sửa và hoàn tất bài viết của họ." + title_max_word_length: "Chiều dài tối đa chữ cho phép, tính theo ký tự, trong một tiêu đề chủ đề." + min_title_similar_length: "Chiều dài tối thiểu của tiêu đề trước khi kiểm tra trùng chủ đề." + min_body_similar_length: "Chiều dài tối thiểu của nội dung bài viết trước khi kiểm trang chủ đề tương tự." + category_colors: "Danh sách mã màu hexa cho phép cho danh mục." + max_attachment_size_kb: "Kích thước file tải lên tối đa tính theo kB. đã cấu hình trong nginx (client_max_body_size) / apache hoặc proxy." + authorized_extensions: "Danh sách định dạng file cho phép tải lên (sử dụng '*' để cho phép tất cả loại tập tin)" + reply_by_email_enabled: "Cho phép trả lời chủ đề qua email." + pop3_polling_ssl: "Sử dụng SSL khi kết nối tới POP3 server. (Đề nghị sử dụng)" + email_in_min_trust: "Bậc tin tưởng tối thiểu cho phép một thành viên gửi chủ đề mới qua email." + username_change_period: "Số ngày thành viên có thể thay đổi tên đăng nhập sau khi đăng kí (0 để vô hiệu hóa chức năng thay đổi tên thành viên)" + email_editable: "Cho phép thành viên thay đổi địa chỉ email sau khi đăng kí" + logout_redirect: "Trang chuyển hướng sau khi đăng xuất . Ví dụ : (http://somesite.com/logout)" + allow_uploaded_avatars: "Cho phép người dùng tải lên hình hồ sơ." + allow_animated_thumbnails: "Tạo ảnh động thu nhỏ cho ảnh .gif" + digest_min_excerpt_length: "Số kí tự tối thiểu của tóm tắt bài viết trong bản tin tóm tắt gửi qua email" + max_daily_gravatar_crawls: "Giới hạn số lần Discourse sẽ kiểm tra Gravatar mới trong một ngày" + allow_profile_backgrounds: "Cho phép người dùng tải lên ảnh nền" + suppress_uncategorized_badge: "Không hiển thị huy hiệu cho các chủ đề chưa phân loại trong danh sách chủ đề" + invites_per_page: "Lời mời mặc định hiển thị trên trang thành viên" + short_progress_text_threshold: "Sau khi số bài đăng của một chủ đề vượt qua giới hạn này, thanh tiến trình sẽ chỉ hiện số thứ tự của bài đăng hiện tại. Nếu bạn thay đổi chiều rộng của thanh tiến trình, bạn có thể cần thay đổi giá trị này" + show_create_topics_notice: "Nếu trang có ít hơn 5 chủ đề công khai, hiển thị một thông báo yêu cầu quản trị tạo thêm các chủ đề mới" + prevent_anons_from_downloading_files: "Cấm khách truy cập tải các tập tin đính kèm. CẢNH BÁO: việc này sẽ chặn những hình ảnh không thuộc giao diện trang hoạt động" + default_email_mailing_list_mode: "Mặc định gửi email cho mỗi bài viết mới." + default_email_always: "Mặc định gửi email thông báo mỗi khi người dùng kích hoạt." + default_other_external_links_in_new_tab: "Mặc định mở các liên kết ngoài trong thẻ mới " + errors: + invalid_email: "Địa chỉ email sai" + invalid_username: "Không có thành viên với tên đăng nhập này" + invalid_integer_min_max: "Giá trị phãi nằm giữa %{min} và %{max}." + invalid_integer_min: "Giá trị phải bằng %{min} hoặc lớn hơn" + invalid_integer_max: "Giá trị không thể cao hơn %{max}." + invalid_integer: "Giá trị phải là một số nguyên" + regex_mismatch: "Giá trị không giống với định dạng" + invalid_string: "Giá trị không hợp lệ." + invalid_string_min_max: "Phải nằm giữa %{min} và %{max} ký tự." + invalid_string_min: "Phải ít nhất %{min} ký tự." + invalid_string_max: "Không nhiều hơn %{max} ký tự." + invalid_reply_by_email_address: "Giá trị phải chứa '%{reply_key}' và phải khác với email thông báo." + notification_types: + mentioned: "%{display_username} đề cập bạn ở %{link}" + liked: "%{display_username} thích bài viết %{link} của bạn" + replied: "%{display_username} trả lời bài viết %{link} của bạn" + quoted: "%{display_username} trích dẫn bài viết %{link} của bạn" + edited: "%{display_username} sửa đổi bài viết %{link} của bạn" + posted: "%{display_username} viết bài ở %{link}" + moved_post: "%{display_username} di chuyển bài viết của bạn tới %{link}" + private_message: "%{display_username} gửi bạn một tin nhắn: %{link}" + invited_to_private_message: "%{display_username} mời bạn xem tin nhắn: %{link}" + invited_to_topic: "%{display_username} mời bạn xem chủ đề: %{link}" + invitee_accepted: "%{display_username} chấp nhận lời mời của bạn" + linked: "%{display_username} kết nối với bạn ở %{link}" + granted_badge: "Bạn kiếm được %{link}" + search: + within_post: "#%{post_number} bởi %{username}" + types: + category: 'Thư mục' + topic: 'Kết quả' + user: 'Thành viên' + sso: + not_found: "Không thể tìm hoặc tạo tài khoản, vui lòng liên hệ quản trị trang" + account_not_approved: "Tài khoản đang chờ duyệt, bạn sẽ nhận được email thông báo khi được duyệt" + unknown_error: "Có lỗi khi cập nhật thông tin, liên hệ quản trị trang" + timeout_expired: "Tài khoản đăng nhập bị quá thời gian, vui lòng đăng nhập lại" + original_poster: "Người viết gốc" + most_posts: "Bài viết Phỏ biến" + redirected_to_top_reasons: + new_user: "Chào mừng đến với cộng dồng của chúng tôi! Ở đây có những chủ để phổ biến." + not_seen_in_a_month: "Chào mừng quay trở lại! Chúng tôi thấy bạn truy cập một khoảng thời gian. Ở đây có những bài viết phổ biến từ lúc bạn đ." + change_owner: + deleted_user: "xóa người dùng" + topic_statuses: + archived_enabled: "Chủ đề này được đưa vào lưu trữ. Nó sẽ không được sửa đổi nữa. " + archived_disabled: "Chủ đề này được đưa khỏi lưu trữ. Nó có thể được sửa đổi." + closed_enabled: "Chủ đề này được đóng lại. Các trả lời mới sẽ không được chấp nhận." + closed_disabled: "Chủ đề này được mở ra. Các trả lời mới sẽ được chấp nhận." + autoclosed_enabled_lastpost_hours: + other: "Chủ đề này đã được đóng tự động %{count} giờ sau phản hồi cuối cùng. Không còn cho phép phản hồi mới." + autoclosed_disabled: "Chủ đề này đã được mở. Bạn có thể bình luận" + autoclosed_disabled_lastpost: "Chủ đề này đã được mở. Bạn có thể bình luận" + visible_enabled: "Chủ để này đã được lưu. Nó sẽ hiển thị trong danh sách chủ đề." + login: + not_approved: "Tài khoản của bạn chưa được kiểm duyệt. Bạn sẽ nhận được email thông báo khi bạn được phép đăng nhập." + incorrect_username_email_or_password: "Không đúng tài khoản, email hoặc mật khẩu" + wait_approval: "Cảm ơn bạn đã đăng ký. Chúng tôi sẽ thông báo sau khi tài khoản của bạn được kiểm duyệt." + active: "Tài khoản của bạn đã được kích hoạt và sẵn sàng để sử dụng." + not_activated: "Bạn không thể đăng nhập bây giờ. Chúng tôi đã gửi bạn một email kích hoạt tài khoản. Vui lòng làm theo hướng dẫn trong email để kích hoạt tài khoản của bạn." + not_allowed_from_ip_address: "Bạn không thể đăng nhập như là %{username} từ địa chỉ IP này." + admin_not_allowed_from_ip_address: "Bạn không thể đăng nhập như quản trị từ IP này." + suspended: "Bạn không thể đăng nhập cho tới ngày %{date}." + suspended_with_reason: "Tài khoản bị tạm khóa cho tới %{date}: %{reason}" + errors: "%{errors}" + not_available: "Không có sẵn. Thử %{suggestion}?" + something_already_taken: "Có lỗi xảy ra, tên đăng nhập hoặc email đã được đăng ký. Thử sử dụng chức năng quên mật khẩu." + omniauth_error: "Xin lỗi, có lỗi khi xác thực tài khoản của bạn. Bạn không được duyệt chứng thực?" + omniauth_error_unknown: "Cố lỗi xảy ra khi bạn đăng nhập, vui lòng thử lại." + new_registrations_disabled: "Đăng ký tài khoản mới không được cho phép tại thời điểm này." + password_too_long: "Mật khẩu giới hạn không quá 200 ký tự." + email_too_long: "Email bạn cung cấp quá dài. Địa chỉ email phải không quá 254 ký tự, và tên miền phải không quá 253 ký tự." + reserved_username: "Tên đăng nhập không được cho phép." + missing_user_field: "Bạn không hoàn tất tất cả các trường người dùng" + close_window: "Chứng thực hoàn tất. Đóng của sổ này để tiếp tụ." + user: + username: + characters: "chỉ bao gồm số, ký tự và dấu gạch dưới" + unique: "phải độc nhất" + blank: "phải hiện hành" + must_begin_with_alphanumeric: "phải bắt đầu bằng ký tự hoặc số hoặc gạch dưới" + must_end_with_alphanumeric: "phải kết thúc bằng ký tự hoặc số hoặc gạch dưới" + must_not_contain_confusing_suffix: "không chứ từ gây hiểu lầm như .json hoặc .png v.v..." + email: + not_allowed: "không được chấp nhận từ nhà cung cấp email đó. Vui long sử dụng địa chỉ email khác." + blocked: "không được chấp nhận." + ip_address: + blocked: "Đăng ký mới không cho phép từ địa chỉ IP của bạn." + invite_forum_mailer: + subject_template: "%{invitee_name} đã mời bạn gia nhập %{site_domain_name}" + text_body_template: | + %{invitee_name} đã mời bạn gia nhập + + > **%{site_title}** + > + > %{site_description} + + Nếu bạn không thích, nhấn vào link dưới đây: + + %{invite_link} + + Nó được mời từ một người dùng tin cập, bạn không cần đăng nhập. + invite_password_instructions: + subject_template: "Đặt mật khẩu cho tài khoản của bạn ở %{site_name}" + new_version_mailer: + subject_template: "[%{site_name}] Phiên bạn Discourse mới, cập nhật đã sẵn sàng" + new_version_mailer_with_notes: + subject_template: "[%{site_name}] cập nhật đã sẵn sàng" + flags_reminder: + please_review: "Vui lòng xem lại chúng." + post_number: "bài đăng" + flags_dispositions: + agreed: "Cảm ơn đã cho chúng tôi biết. Chúng thôi đồng ý nó là một vấn đề và chúng tôi sẽ xem xét nó." + agreed_and_deleted: "Cảm ơn đã cho chúng tôi biết thông tin. Chúng tôi đồng ý đây là một vấn đề và chúng tôi đã xóa bài viết này." + disagreed: "Cảm ơn đã cho chúng tôi biết thông tin. Chúng tôi đang xem xét nó." + deferred: "Cảm ơn đã cho chúng tôi biết thông tin. Chúng tôi đang xem xét nó." + system_messages: + welcome_user: + subject_template: "Chào mừng đến với %{site_name}!" + welcome_invite: + subject_template: "Chào mừng đến với %{site_name}!" + backup_succeeded: + subject_template: "Sản sao lưu hoàn tất thành công" + backup_failed: + subject_template: "Sao lưu lỗi." + text_body_template: | + Sao lưu lỗi. + + Đây là log: + + ``` + %{logs} + ``` + restore_succeeded: + subject_template: "Phục hồi thành công" + text_body_template: "Phục hồi đã thành công." + restore_failed: + subject_template: "Phục hồi thất bại." + text_body_template: | + Phục hồi thất bại. + + Đây là log: + + ``` + %{logs} + ``` + csv_export_succeeded: + subject_template: "Xuất dữ liệu hoàn tất" + csv_export_failed: + subject_template: "Xuất dữ liệu thất bại" + text_body_template: "Chúng tôi xin lỗi, những dữ liệu bạn xuất bị lỗi. Vui lòng xem log hoặc liên hệ nhân viên." + email_reject_no_account: + subject_template: "[%{site_name}] Vấn đề Email -- Không xác định tài khoản" + email_reject_empty: + subject_template: "[%{site_name}] Vấn đề Email -- Không có nội dung" + email_reject_parsing: + subject_template: "[%{site_name}] Vấn đề Email-- Không nhận dạng được nội dung" + email_reject_invalid_access: + subject_template: "[%{site_name}] Vấn đề Email -- truy cập không phù hợp" + email_reject_post_error: + subject_template: "[%{site_name}] Vấn đề Email -- Lỗi đăng bài" + email_reject_post_error_specified: + subject_template: "[%{site_name}] Vấn đề Email -- Lỗi đăng bài" + email_reject_reply_key: + subject_template: "[%{site_name}] Vấn đề Email -- Không xác định được key trả lời" + email_reject_destination: + subject_template: "[%{site_name}] Vấn đề Email -- Không xác định địa chỉ Đến:" + email_reject_topic_not_found: + subject_template: "[%{site_name}] Vấn đề Email -- Không tìm thấy chủ đề" + email_reject_topic_closed: + subject_template: "[%{site_name}] Vấn đề Email -- Chủ đề đóng" + email_reject_auto_generated: + subject_template: "[%{site_name}] Vấn đề Email -- Tự động tạo trả lời" + email_error_notification: + subject_template: "[%{site_name}] Vấn đề Email -- chứng thực POP lỗi" + too_many_spam_flags: + subject_template: "Tài khoản mới bị chặn" + blocked_by_staff: + subject_template: "Tài khoản bị khóa" + unblocked: + subject_template: "Tài khoản được mở khóa" + pending_users_reminder: + subject_template: + other: "%{count} thành viên đang chờ duyệt" + subject_re: "Re:" + subject_pm: "[PM]" + user_notifications: + previous_discussion: "Các trả lời trước" + unsubscribe: + title: "Bỏ theo dõi" + description: "Bạn không thích nhận mail giống mail này? Nhấn vào bỏ theo dõi để bỏ đăng ký ngay lập tức:" + posted_by: "Đăng bởi %{{username} ngày %{post_date}" + user_invited_to_private_message_pm: + subject_template: "[%{site_name}] %{username} mời bạn trả lời thông điệp '%{topic_title}'" + user_invited_to_topic: + subject_template: "[%{site_name}] %{username} mời bạn trả lời chủ đề '%{topic_title}'" + user_replied: + subject_template: "[%{site_name}] %{topic_title}" + user_quoted: + subject_template: "[%{site_name}] %{topic_title}" + user_mentioned: + subject_template: "[%{site_name}] %{topic_title}" + user_posted: + subject_template: "[%{site_name}] %{topic_title}" + user_posted_pm: + subject_template: "[%{site_name}] [PM] %{topic_title}" + digest: + why: "Tóm tắt %{site_link} từ lần cuối truy cập %{last_seen_at}" + subject_template: "[%{site_name}] Tóm tắt" + new_activity: "Hoạt động mới ở chủ đề và bài viết của bạn:" + top_topics: "Bài viết phổ biến" + other_new_topics: "Chủ đề phổ biến" + click_here: "bấm vào đây" + from: "%{site_name} tóm tắt" + read_more: "Đọc Tiếp" + more_topics: "Đây là %{new_topics_since_seen} những chủ đề mới khác." + more_topics_category: "Thêm chủ đề mới:" + forgot_password: + subject_template: "[%{site_name}] Đặt lại mật khẩu" + set_password: + subject_template: "[%{site_name}] Đặt Mật khẩu" + admin_login: + subject_template: "[%{site_name}] Đăng nhập" + account_created: + subject_template: "[%{site_name}] Tài khoản mới" + authorize_email: + subject_template: "[%{site_name}] Xác nhận địa chỉ email mới của bạn" + signup_after_approval: + subject_template: "Bạn đã được kiểm duyệt ở %{site_name}!" + signup: + subject_template: "[%{site_name}] Xác nhận tài khoản mới của bạn" + page_not_found: + title: "Trang bạn yêu cầu không tồn tại hoặc riêng tư." + popular_topics: "Phổ biến" + recent_topics: "Gân đây" + see_more: "Thêm" + search_title: "Tìm trang này" + search_google: "Goole" + login_required: + welcome_message: | + #[Chào mừng đến %{title}](#welcome) + Trang này yêu cầu phải có tài khoản. Vui lòng tạo một tài khoản hoặc đăng nhập để tiếp tục. + terms_of_service: + title: "Điều khoản Dịch vụ" + signup_form_message: 'Tôi đã đọc và đồng ý với Điều khoản dịch vụ.' + deleted: 'đã bị xóa ' + upload: + edit_reason: "tải về một bản sao của hình ảnh." + unauthorized: "Xin lỗi, tập tin của bạn tải lên không được cho phép (định dạng cho phép: %{authorized_extensions})." + pasted_image_filename: "Hình ảnh được chèn" + store_failure: "Tải lên lỗi #%{upload_id} cho tài khoản #%{user_id}." + file_missing: "Xin lỗi, bạn phải cung cấp tập tin để tải lên" + attachments: + too_large: "Xin lỗi, tập tin bạn tải lên quá lớn (kích thước tối đa %{max_size_kb}KB)." + images: + too_large: "Xin lỗi, hình bạn tải lên quá lớn (kích thước tối đa %{max_size_kb}KB), Vui lòng chỉnh lại kích thước và thử lại." + size_not_found: "Xin lỗi, không thể xác định kích thước hình. Có thể hình của bạn bị lỗi?" + avatar: + missing: "Xin lỗi, hình đại diện bạn chọn không có sãn trên máy chủ. Bạn có thể tải lên lại?" + email_log: + no_user: "không tìm thấy người dùng với id %{user_id}" + anonymous_user: "Người dùng là nặc danh" + suspended_not_pm: "Tài khoản bị tạm khóa, không có tin nhắn" + seen_recently: "Tài khoản đã xem gần đây" + post_not_found: "Không tìm thấy bài viết với id %{post_id}" + notification_already_read: "Thông báo email này đã được đọc" + topic_nil: "post.topic is nil" + post_deleted: "bài viết đã bị xóa bởi tác giả" + user_suspended: "người dùng đã bị tạm khóa" + already_read: "người dùng đã đọc bài viết này" + message_blank: "tin nhắn rỗng" + message_to_blank: "message.to rỗng" + text_part_body_blank: "text_part.body rỗng" + body_blank: "nội dung rỗng" + color_schemes: + base_theme_name: "Cơ bản" + about: "Giới thiệu" + guidelines: "Hướng dẫn" + privacy: "Riêng tư" + edit_this_page: "Sửa trang này" + csv_export: + boolean_yes: "Đồng ý" + boolean_no: "Không" + guidelines_topic: + title: "FAQ/Hướng dẫn" + tos_topic: + title: "Điều khoản Dịch vụ" + privacy_topic: + title: "Chính sách Riêng tư" + admin_login: + success: "Gửi mail lỗi" + error: "Lỗi!" + email_input: "Email quản trị" + submit_button: "Gửi email" + performance_report: + initial_topic_title: Báo cáo hiệu suất website + diff --git a/plugins/poll/config/locales/client.vi_VN.yml b/plugins/poll/config/locales/client.vi_VN.yml new file mode 100644 index 0000000000..1bbdb53fdf --- /dev/null +++ b/plugins/poll/config/locales/client.vi_VN.yml @@ -0,0 +1,31 @@ +vi_VN: + js: + poll: + voters: + other: "người bình chọn" + total_votes: + other: "tổng số bình chọn" + average_rating: "Đánh giá trung bình: %{average}." + multiple: + help: + between_min_and_max_options: "Bạn có thể chọn giữa %{min}%{max}." + cast-votes: + title: "Bỏ phiếu của bạn" + label: "Bình chọn ngay!" + show-results: + title: "Hiển thị kết quả cuộc thăm dò" + label: "Hiện kết quả" + hide-results: + title: "Trở lại bầu chọn của bạn" + label: "Ẩn kết quả" + open: + title: "Mở bình chọn" + label: "Mở" + confirm: "Bạn có chắc mở bình chọn này?" + close: + title: "Đóng bình chọn" + label: "Đóng lại" + confirm: "Bạn có chắc chắn muốn đóng bình chọn này?" + error_while_toggling_status: "Có lỗi trong khi chuyển đổi qua lại các trạng thái của bình chọn này." + error_while_casting_votes: "Có lỗi trong khi tạo mãu bầu chọn của bạn" + diff --git a/plugins/poll/config/locales/server.vi_VN.yml b/plugins/poll/config/locales/server.vi_VN.yml new file mode 100644 index 0000000000..52ccdf117c --- /dev/null +++ b/plugins/poll/config/locales/server.vi_VN.yml @@ -0,0 +1,25 @@ +vi_VN: + site_settings: + poll_enabled: "Cho phép người dùng tạo các cuộc thăm dò?" + poll_maximum_options: "Số lượng tối đa tùy chọn trong một cuộc thăm dò." + poll: + multiple_polls_without_name: "Có nhiều cuộc thăm dò mà không có tên. Sử dụng thuộc tính 'name' để xác định cuộc thăm dò của bạn." + multiple_polls_with_same_name: "Có nhiều cuộc thăm dò có cùng tên: %{name}. Sử dụng thuộc tính 'name' để xác định cuộc thăm dò của bạn." + default_poll_must_have_at_least_2_options: "Thăm dò ý kiến ​​phải có ít nhất 2 lựa chọn." + named_poll_must_have_at_least_2_options: "Thăm dò có tên %{name} phải có ít nhất 2 lựa chọn." + default_poll_must_have_different_options: "Thăm dò ý kiến ​​phải có các tùy chọn khác nhau." + named_poll_must_have_different_options: "Thăm dò %{name} ​​phải có các tùy chọn khác nhau." + default_poll_with_multiple_choices_has_invalid_parameters: "Thăm dò ý kiến ​​với nhiều sự lựa chọn có các tham số không hợp lệ." + named_poll_with_multiple_choices_has_invalid_parameters: "Thăm dò %{name} ​​với nhiều sự lựa chọn có các tham số không hợp lệ. " + requires_at_least_1_valid_option: "Bạn phải chọn ít nhất 1 lựa chọn hợp lệ." + cannot_change_polls_after_5_minutes: "Bạn không thể thêm, xóa hoặc đổi tên các cuộc thăm dò 5 phút đầu tiên." + op_cannot_edit_options_after_5_minutes: "Bạn không thể thêm hoặc loại bỏ các bình chọn sau khi 5 phút đầu tiên. Hãy liên hệ với người điều hành nếu bạn cần chỉnh sửa một bình chọn nào đó." + staff_cannot_add_or_remove_options_after_5_minutes: "Bạn không thể thêm hoặc loại bỏ các bình chọn sau khi 5 phút đầu tiên. Bạn nên đóng chủ đề này và tạo ra một cái mới để thay thế." + no_polls_associated_with_this_post: "Không có cuộc thăm dò được liên kết với bài này." + no_poll_with_this_name: "Không có thăm dò có tên %{name} liên kết với bài viết này." + post_is_deleted: "Không thể thực hiện trên bài viết đã xóa." + topic_must_be_open_to_vote: "Các chủ đề phải được mở để bầu chọn." + poll_must_be_open_to_vote: "Thăm dò ý kiến ​​phải được mở để bầu chọn." + topic_must_be_open_to_toggle_status: "Các chủ đề phải được mở để chuyển trạng thái." + only_staff_or_op_can_toggle_status: "Chỉ có một BQT hoặc các người đăng bài có thể chuyển đổi một trạng thái thăm dò ý kiến" + diff --git a/public/403.vi_VN.html b/public/403.vi_VN.html new file mode 100644 index 0000000000..09a5aa396b --- /dev/null +++ b/public/403.vi_VN.html @@ -0,0 +1,27 @@ + + + Bạn không thể làm được điều đó (403) + + + + +
+

403

+

Bạn không thể xem tài nguyên đó!

+ +

Trang này sẽ được thay thế bằng một trang lỗi 403 tùy chỉnh của Discourse.

+
+ + + diff --git a/public/422.vi_VN.html b/public/422.vi_VN.html new file mode 100644 index 0000000000..483475327c --- /dev/null +++ b/public/422.vi_VN.html @@ -0,0 +1,26 @@ + + +Thay đổi bạn muốn thực hiện đã bị từ chối (422) + + + + + +
+

Thay đổi bạn muốn thực hiện đã bị từ chối.

+

Có thể bạn đã cố thay đổi một số tính năng mà bạn không thể truy cập.

+
+ + + diff --git a/public/500.vi_VN.html b/public/500.vi_VN.html new file mode 100644 index 0000000000..831451072a --- /dev/null +++ b/public/500.vi_VN.html @@ -0,0 +1,13 @@ + + +Ôi - Lỗi 500 + + + +

Ối

+

Phần mềm chạy diễn đàn thảo luận này bất ngờ gặp sự cố không mong muốn. Chúng tôi rất xin lỗi vì sự bất tiện này.

+

Thông tin chi tiết về lỗi đã được ghi lại và thông báo tự động đã được tạo. Chúng tôi sẽ xem xét lỗi này.

+

Không cần tiến hành bất cứ hành động nào. Tuy nhiên, nếu lỗi này vẫn tiếp tục, bạn có thể cung cấp thêm thông tin chi tiết bao gồm các bước để tái tạo lỗi hoặc tạo một thảo luận trên meta category.

+ + + diff --git a/public/503.vi_VN.html b/public/503.vi_VN.html new file mode 100644 index 0000000000..85ca43edc7 --- /dev/null +++ b/public/503.vi_VN.html @@ -0,0 +1,12 @@ + + +Trang đang được bảo trì- Discourse.org + + + +

Trang tạm dừng dịch vụ để bảo trì dịch vụ

+

Vui lòng quay lại sau vài phút.

+

Xin lỗi về sự bất tiện này!

+ + + diff --git a/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi_VN.yml b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi_VN.yml new file mode 100644 index 0000000000..7bbf9c106b --- /dev/null +++ b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi_VN.yml @@ -0,0 +1,6 @@ +vi_VN: + site_settings: + enable_imgur: "Kích hoạt imgur api để tải file lên, không lưu trữ file tại máy chủ." + imgur_client_id: "Client ID imgur.com của bạn, cần cho chức năng tải ảnh lên. " + imgur_client_secret: "Client secret của bạn tạo imgur.com. Hiện tại không cần để tải ảnh lên, nhưng sẽ có trong tương lai." + From 08801b835cb7248ecc43b8fc5522b3f9e4ec9a54 Mon Sep 17 00:00:00 2001 From: scossar Date: Sun, 17 Jan 2016 12:36:15 -0800 Subject: [PATCH 002/140] put avatar in nested table --- app/views/email/_post.html.erb | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/app/views/email/_post.html.erb b/app/views/email/_post.html.erb index 75510c19a9..c081fb5afd 100644 --- a/app/views/email/_post.html.erb +++ b/app/views/email/_post.html.erb @@ -1,19 +1,25 @@ - From 2a9c7d0099ef8acf1dc5ac55de8298c95c183e95 Mon Sep 17 00:00:00 2001 From: "Khoa, Le Ngoc" Date: Fri, 22 Jan 2016 14:43:53 +0700 Subject: [PATCH 003/140] Update tranlation code --- config/locales/client.vi.yml | 2088 +++++++++++++++++ config/locales/server.vi.yml | 1027 ++++++++ plugins/poll/config/locales/client.vi.yml | 37 + plugins/poll/config/locales/server.vi.yml | 31 + public/403.vi.html | 26 + public/422.vi.html | 25 + public/500.vi.html | 12 + public/503.vi.html | 11 + .../lib/discourse_imgur/locale/server.vi.yml | 12 + 9 files changed, 3269 insertions(+) create mode 100644 config/locales/client.vi.yml create mode 100644 config/locales/server.vi.yml create mode 100644 plugins/poll/config/locales/client.vi.yml create mode 100644 plugins/poll/config/locales/server.vi.yml create mode 100644 public/403.vi.html create mode 100644 public/422.vi.html create mode 100644 public/500.vi.html create mode 100644 public/503.vi.html create mode 100644 vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml new file mode 100644 index 0000000000..041289b078 --- /dev/null +++ b/config/locales/client.vi.yml @@ -0,0 +1,2088 @@ +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + +vi: + js: + number: + format: + separator: "." + delimiter: "," + human: + storage_units: + format: '%n %u' + units: + byte: + other: Byte + gb: GB + kb: KB + mb: MB + tb: TB + short: + thousands: "{{number}}k" + millions: "{{number}}M" + dates: + time: "h:mm a" + long_no_year: "MMM D h:mm a" + long_no_year_no_time: "MMM D" + full_no_year_no_time: "MMMM Do" + long_with_year: "MMM D, YYYY h:mm a" + long_with_year_no_time: "MMM D, YYYY" + full_with_year_no_time: "MMMM Do, YYYY" + long_date_with_year: "MMM D, 'YY LT" + long_date_without_year: "MMM D, LT" + long_date_with_year_without_time: "MMM D, 'YY" + long_date_without_year_with_linebreak: "MMM D
LT" + long_date_with_year_with_linebreak: "MMM D, 'YY
LT" + tiny: + half_a_minute: "< 1m" + less_than_x_seconds: + other: "< %{count}s" + x_seconds: + other: "%{count}s" + less_than_x_minutes: + other: "< %{count}m" + x_minutes: + other: "%{count}m" + about_x_hours: + other: "%{count}h" + x_days: + other: "%{count}d" + about_x_years: + other: "%{count}y" + over_x_years: + other: "> %{count}y" + almost_x_years: + other: "%{count}y" + date_month: "MMM D" + date_year: "MMM 'YY" + medium: + x_minutes: + other: "%{count} phút" + x_hours: + other: "%{count} giờ" + x_days: + other: "%{count} ngày" + date_year: "MMM D, 'YY" + medium_with_ago: + x_minutes: + other: " %{count} phút trước" + x_hours: + other: "%{count} giờ trước" + x_days: + other: "%{count} ngày trước" + later: + x_days: + other: "còn %{count} ngày" + x_months: + other: "còn %{count} tháng" + x_years: + other: "còn %{count} năm" + share: + topic: 'chia sẽ chủ đề này' + post: 'đăng #%{Bài đăng số}' + close: 'đóng lại' + twitter: 'chia sẽ lên Twitter' + facebook: 'chia sẽ lên Facebook' + google+: 'chia sẽ lên Google+' + email: 'Gửi liên kết này qua thư điện tử' + action_codes: + split_topic: "chìa chủ đề này lúc %{when}" + autoclosed: + enabled: 'đóng lúc %{when}' + disabled: 'mở lúc %{when}' + closed: + enabled: 'đóng lúc %{when}' + disabled: 'mở lúc %{when}' + archived: + enabled: 'lưu trữ %{when}' + disabled: 'bỏ lưu trữ %{when}' + pinned: + enabled: 'gắn lúc %{when}' + disabled: 'bỏ gim %{when}' + pinned_globally: + enabled: 'đã gắn toàn trang %{when}' + disabled: 'đã bỏ gắn %{when}' + visible: + enabled: 'đã lưu %{when}' + disabled: 'bỏ lưu %{when}' + topic_admin_menu: "quản lí chủ đề." + emails_are_disabled: "Ban quản trị đã chặn mọi email đang gửi. Sẽ không có bắt kỳ thông báo nào về email được gửi đi." + edit: 'thay đổi tiêu đề và chuyên mục của chủ đề' + not_implemented: "Tính năng này chưa được hoàn thiện hết, xin lỗi!" + no_value: "Không" + yes_value: "Có" + generic_error: "Rất tiếc, đã có lỗi xảy ra." + generic_error_with_reason: "Đã xảy ra lỗi: %{error}" + sign_up: "Đăng ký" + log_in: "Đăng nhập" + age: "Độ tuổi" + joined: "Đã tham gia" + admin_title: "Quản trị" + flags_title: "Báo cáo" + show_more: "hiện thêm" + show_help: "lựa chọn" + links: "Liên kết" + links_lowercase: + other: "liên kết" + faq: "FAQ" + guidelines: "Hướng dẫn" + privacy_policy: "Chính sách riêng tư" + privacy: "Sự riêng tư" + terms_of_service: "Điều khoản dịch vụ" + mobile_view: "Xem ở chế độ di động" + desktop_view: "Xem ở chế độ máy tính" + you: "Bạn" + or: "hoặc" + now: "ngay lúc này" + read_more: 'đọc thêm' + more: "Nhiều hơn" + less: "Ít hơn" + never: "không bao giờ" + daily: "hàng ngày" + weekly: "hàng tuần" + every_two_weeks: "mỗi hai tuần" + every_three_days: "ba ngày một" + max_of_count: "tối đa của {{count}}" + alternation: "hoặc" + character_count: + other: "{{count}} ký tự" + suggested_topics: + title: "Chủ đề tương tự" + about: + simple_title: "Giới thiệu" + title: "Giới thiệu về %{title}" + stats: "Thống kê của trang" + our_admins: "Các quản trị viên" + our_moderators: "Các điều hành viên" + stat: + all_time: "Từ trước tới nay" + last_7_days: "7 ngày qua" + last_30_days: "30 ngày vừa qua" + like_count: "Lượt thích" + topic_count: "Các chủ đề" + post_count: "Các bài viết" + user_count: "Thành viên mới" + active_user_count: "Thành viên tích cực" + contact: "Contact Us" + contact_info: "Trong trường hợp có bất kỳ sự cố nào ảnh hưởng tới trang này, xin vui lòng liên hệ với chúng tôi theo địa chỉ %{contact_info}." + bookmarked: + title: "Bookmark" + clear_bookmarks: "Clear Bookmarks" + help: + bookmark: "Chọn bài viết đầu tiên của chủ đề cho vào bookmark" + unbookmark: "Chọn để xoá toàn bộ bookmark trong chủ đề này" + bookmarks: + not_logged_in: "rất tiếc, bạn phải đăng nhập để có thể đánh dấu bài viết" + created: "bạn đã đánh dấu bài viết này" + not_bookmarked: "bạn đã đọc bài viết này; nhấp chuột để đánh dấu" + last_read: "đây là bài viết cuối cùng bạn đã đọc; nhấp chuột để đánh dấu" + remove: "Xóa đánh dấu" + confirm_clear: "Bạn có chắc muốn xóa tất cả đánh dấu ở topic này?" + topic_count_latest: + other: "{{count}} chủ đề mới hoặc đã cập nhật." + topic_count_unread: + other: "{{count}} chủ đề chưa đọc." + topic_count_new: + other: "{{count}} chủ đề mới." + click_to_show: "Nhấp chuột để hiển thị." + preview: "xem trước" + cancel: "hủy" + save: "Lưu thay đổi" + saving: "Đang lưu ..." + saved: "Đã lưu!" + upload: "Tải lên" + uploading: "Đang tải lên..." + uploading_filename: "Đang tải lên {{filename}}..." + uploaded: "Đã tải lên!" + enable: "Kích hoạt" + disable: "Vô hiệu hóa" + undo: "Hoàn tác" + revert: "Phục hồi" + failed: "Thất bại" + switch_to_anon: "Chế độ Ẩn danh" + switch_from_anon: "Thoát ẩn danh" + banner: + close: "Xóa biểu ngữ này." + edit: "Sửa banner này >>" + choose_topic: + none_found: "Không tìm thấy chủ đề nào" + title: + search: "Tìm kiếm chủ đề dựa vào tên, url hoặc id:" + placeholder: "viết tiêu đề của chủ đề thảo luận ở đây" + queue: + topic: "Chủ đề" + approve: 'Phê duyệt' + reject: 'Từ chối' + delete_user: 'Xóa tài khoản' + title: "Cần phê duyệt" + none: "Không có bài viết nào để xem trước" + edit: "Sửa" + cancel: "Hủy" + view_pending: "xem bài viết đang chờ xử lý" + has_pending_posts: + other: "Chủ đề này có {{count}} bài viết cần phê chuẩn" + confirm: "Lưu thay đổi" + delete_prompt: "Bạn có chắc muốn xóa %{username}? Tất cả các bài viết của họ sẽ bị xóa, email và ip sẽ bị chặn." + approval: + title: "Bài viết cần phê duyệt" + description: "Chúng tôi đã nhận được bài viết mới của bạn, nhưng nó cần phải được phê duyệt bởi admin trước khi được hiện. Xin hãy kiên nhẫn." + pending_posts: + other: "Bạn có {{count}} bài viết đang chờ xử lý." + ok: "OK" + user_action: + user_posted_topic: "{{user}} đăng chủ đề" + you_posted_topic: "Bạn đã đăng chủ đề" + user_replied_to_post: "{{user}} đã trả lời tới {{post_number}}" + you_replied_to_post: "Bạn đã trả lời tới {{post_number}}" + user_replied_to_topic: "{{user}} đã trả lời chủ đề" + you_replied_to_topic: "Bạn đã trả lời tới chủ đề" + user_mentioned_user: "{{user}} đã nhắc đến {{another_user}}" + user_mentioned_you: "{{user}} đã nhắc tới bạn" + you_mentioned_user: "Bạn đã đề cập {{another_user}}" + posted_by_user: "Được đăng bởi {{user}}" + posted_by_you: "Được đăng bởi bạn" + sent_by_user: "Đã gửi bởi {{user}}" + sent_by_you: "Đã gửi bởi bạn" + directory: + filter_name: "lọc theo tên đăng nhập" + title: "Thành viên" + likes_given: "Đưa ra" + likes_received: "Đã nhận" + topics_entered: "Được nhập" + topics_entered_long: "Chủ đề được nhập" + time_read: "Thời gian đọc" + topic_count: "Chủ đề" + topic_count_long: "Chủ đề đã được tạo" + post_count: "Trả lời" + post_count_long: "Trả lời đã được đăng" + no_results: "Không tìm thấy kết quả." + days_visited: "Ghé thăm" + days_visited_long: "Ngày đã ghé thăm" + posts_read: "Đọc" + posts_read_long: "Đọc bài viết" + total_rows: + other: "%{count} người dùng" + groups: + visible: "Mọi thành viên có thể nhìn thấy nhóm" + title: + other: "các nhóm" + members: "Các thành viên" + posts: "Các bài viết" + alias_levels: + nobody: "Không ai cả" + only_admins: "Chỉ các quản trị viên" + mods_and_admins: "Chỉ có người điều hành và ban quản trị" + members_mods_and_admins: "Chỉ có thành viên trong nhóm, ban điều hành, và ban quản trị" + everyone: "Mọi người" + trust_levels: + title: "Cấp độ tin tưởng tự động tăng cho thành viên khi họ thêm:" + none: "Không có gì" + user_action_groups: + '1': "Lần thích" + '2': "Lần được thích" + '3': "Chỉ mục" + '4': "Các chủ đề" + '5': "Trả lời" + '6': "Phản hồi" + '7': "Được nhắc đến" + '9': "Lời trích dẫn" + '11': "Biên tập" + '12': "Bài đã gửi" + '13': "Hộp thư" + '14': "Đang chờ xử lý" + categories: + all: "tất cả chuyên mục" + all_subcategories: "Tất cả" + no_subcategory: "không có gì" + category: "Chuyên mục" + reorder: + title: "Sắp xếp lại danh mục" + title_long: "Tổ chức lại danh sách danh mục" + fix_order: "Vị trí cố định" + fix_order_tooltip: "Không phải tất cả danh mục có vị trí duy nhất, điều này có thể dẫn đến kết quả không mong muốn." + save: "Lưu thứ tự" + apply_all: "Áp dụng" + position: "Vị trí" + posts: "Bài viết" + topics: "Chủ đề" + latest: "Mới nhất" + latest_by: "mới nhất bởi" + toggle_ordering: "chuyển lệnh kiểm soát" + subcategories: "Phân loại phụ" + topic_stats: "Số lượng chủ đề mới" + topic_stat_sentence: + other: "%{count} số lượng chủ đề mới tỏng quá khứ %{unit}." + post_stats: "Số lượng bài viết mới" + post_stat_sentence: + other: "%{count} số lượng bài viết mới trong quá khứ %{unit}." + ip_lookup: + title: Tìm kiếm địa chỉ IP + hostname: Hostname + location: Vị trí + location_not_found: (không biết) + organisation: Công ty + phone: Điện thoại + other_accounts: "Tài khoản khác với địa chỉ IP này" + delete_other_accounts: "Xoá %{count}" + username: "tên đăng nhập" + trust_level: "TL" + read_time: "thời gian đọc" + topics_entered: "chủ để đã xem" + post_count: "# bài viết" + confirm_delete_other_accounts: "Bạn có muốn xóa những tài khoản này không?" + user_fields: + none: "(chọn một tùy chọn)" + user: + said: "{{username}}:" + profile: "Tiểu sử" + mute: "Im lặng" + edit: "Tùy chỉnh" + download_archive: "Tải bài viết về" + new_private_message: "Tin nhắn mới" + private_message: "Tin nhắn" + private_messages: "Các tin nhắn" + activity_stream: "Hoạt động" + preferences: "Tùy chỉnh" + expand_profile: "Mở" + bookmarks: "Theo dõi" + bio: "Về tôi" + invited_by: "Được mời bởi" + trust_level: "Độ tin tưởng" + notifications: "Thông báo" + desktop_notifications: + label: "Desktop Notifications" + not_supported: "Xin lỗi. Trình duyệt của bạn không hỗ trợ Notification." + perm_default: "Mở thông báo" + perm_denied_btn: "Không có quyền" + perm_denied_expl: "Bạn bị từ chối quyền cho notification. Dùng trình duyệt của bạn để kích hoạt notification, sau đó nhấp nút này khi hoàn thành. (Desktop: Icon bên trái của thanh địa chỉ. Mobile: 'Site Info'.)" + disable: "Khóa Notification" + currently_enabled: "(đang cho phép)" + enable: "Cho phép Notification" + currently_disabled: "(hiện tại không cho phép)" + each_browser_note: "Lưu ý: Bạn phải thay đổi trong cấu hình mỗi trình duyệt bạn sử dụng." + dismiss_notifications: "Đánh dấu đã đọc cho tất cả" + dismiss_notifications_tooltip: "Đánh dấu đã đọc cho tất cả các thông báo chưa đọc" + disable_jump_reply: "Đừng tới bài viết của tôi sau khi tôi trả lời" + dynamic_favicon: "Hiện số chủ đề mới / cập nhật vào biểu tượng trình duyệt" + edit_history_public: "Để thành viên khác xem những sửa chữa bài viết của bạn" + external_links_in_new_tab: "Mở tất cả liên kết bên ngoài trong thẻ mới" + enable_quoting: "Bật chế độ làm nổi bật chữ trong đoạn trích dẫn trả lời" + change: "thay đổi" + moderator: "{{user}} trong ban quản trị" + admin: "{{user}} là người điều hành" + moderator_tooltip: "Thành viên này là MOD" + admin_tooltip: "Thành viên này là admin" + blocked_tooltip: "Tài khoản này bị khóa" + suspended_notice: "Thành viên này bị đình chỉ cho đến ngày {{date}}. " + suspended_reason: "Lý do: " + github_profile: "Github" + mailing_list_mode: "Gửi email cho tôi mỗi bài viết mới (trừ khi tôi tắt chủ đề hoặc chuyên mục)" + watched_categories: "Xem" + watched_categories_instructions: "Bạn sẽ tự động xem tất cả các chủ đề mới trong các chuyên mục này. Bạn sẽ được thông báo về tất các các bài viết mới, và một số các bài viết mới cũng sẽ xuất hiện ở chủ đề kế tiếp." + tracked_categories: "Theo dõi" + tracked_categories_instructions: "Bạn sẽ tự động theo dõi tất cả các chủ đề trong các chuyên mục này. Một số bài viết mới sẽ xuất hiện ở chủ đề kế tiếp." + muted_categories: "Im lặng" + delete_account: "Xoá Tài khoản của tôi" + delete_account_confirm: "Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của bạn? Hành động này không thể được hoàn tác!" + deleted_yourself: "Tài khoản của bạn đã được xóa thành công." + delete_yourself_not_allowed: "Bạn không thể xóa tài khoản của bạn ngay bây giờ. Liên lạc với admin để làm xóa tài khoản cho bạn." + unread_message_count: "Tin nhắn" + admin_delete: "Xoá" + users: "Thành viên" + muted_users: "Im lặng" + muted_users_instructions: "Ngăn chặn tất cả các thông báo từ những thành viên." + staff_counters: + flags_given: "cờ hữu ích" + flagged_posts: "bài viết gắn cờ" + deleted_posts: "bài viết bị xoá" + suspensions: "đình chỉ" + warnings_received: "cảnh báo" + messages: + all: "Tất cả" + change_password: + success: "(email đã gửi)" + in_progress: "(đang gửi email)" + error: "(lỗi)" + action: "Gửi lại mật khẩu tới email" + set_password: "Nhập Mật khẩu" + change_about: + title: "Thay đổi thông tin về tôi" + error: "Có lỗi khi thay đổi giá trị này." + change_username: + title: "Thay Username" + confirm: "Nếu bạn thay đổi tên đăng nhập, tất cả những câu nói của bạn ở bài viết trước và @tên bạn sẽ được đề cập sẽ bị phá vỡ. Bạn có chắc bạn muốn tiếp tục không?" + taken: "Xin lỗi, đã có username này." + error: "Có lỗi trong khi thay đổi username của bạn." + invalid: "Username này không thích hợp. Nó chỉ chứa các ký tự là chữ cái và chữ số. " + change_email: + title: "Thay đổi Email" + taken: "Xin lỗi, email này không dùng được. " + error: "Có lỗi xảy ra khi thay đổi email của bạn. Có thể địa chỉ email đã được sử dụng ?" + success: "Chúng tôi đã gửi email tới địa chỉ đó. Vui lòng làm theo chỉ dẫn để xác nhận lại." + change_avatar: + title: "Đổi ảnh đại diện" + gravatar: "dựa trên Gravatar" + gravatar_title: "Đổi ảnh đại diện của bạn trên website Gravatar" + refresh_gravatar_title: "Làm mới Gravatar của bạn" + letter_based: "Hệ thống xác định ảnh đại diện" + uploaded_avatar: "Chính sửa hình ảnh" + uploaded_avatar_empty: "Thêm một ảnh chỉnh sửa" + upload_title: "Upload hình ảnh của bạn" + upload_picture: "Úp hình" + image_is_not_a_square: "Cảnh báo: chúng tôi đã cắt hình ảnh của bạn; chiều rộng và chiều cao không bằng nhau." + cache_notice: "Hình hồ sở của bạn đã thay đổi thành công nhưng có thể thỉnh thoảng xuất hiện ảnh cũ bởi cache của trình duyệt." + change_profile_background: + title: "Hình nền trang hồ sơ" + instructions: "Hình nền trang hồ sơ sẽ ở giữa và có chiều rộng mặc định là 850px." + change_card_background: + title: "Hình nền Card" + instructions: "Hình nền sẽ ở giữa và có chiều rộng mặc định là 590px." + email: + title: "Email" + instructions: "Không bao giờ công khai" + ok: "Chúng tôi sẽ gửi thư điện tử xác nhận đến cho bạn" + invalid: "Vùi lòng nhập một thư điện tử hợp lệ" + authenticated: "Thư điện tử của bạn đã được xác nhận bởi {{provider}}" + name: + title: "Tên" + instructions: "Tên đầy đủ của bạn (tùy chọn)" + instructions_required: "Tên đầy đủ của bạn" + too_short: "Tên của bạn quá ngắn" + ok: "Tên của bạn có vẻ ổn" + username: + title: "Username" + instructions: "Duy nhất, không khoảng trắng" + short_instructions: "Mọi người có thể nhắc tới bạn bằng @{{username}}" + available: "Tên đăng nhập của bạn có sẵn" + global_match: "Email đúng với username đã được đăng ký" + global_mismatch: "Đã đăng ký rồi. Thử {{suggestion}}?" + not_available: "Chưa có sẵn. Thử {{suggestion}}?" + too_short: "Tên đăng nhập của bạn quá ngắn" + too_long: "Tên đăng nhập của bạn quá dài" + checking: "Đang kiểm tra username sẵn sàng để sử dụng...." + enter_email: 'Đã tìm được tên đăng nhập. Điền thư điện tử phù hợp.' + prefilled: "Thư điện tử trủng với tên đăng nhập này." + locale: + title: "Ngôn ngữ hiển thị" + instructions: "Ngôn ngữ hiển thị sẽ thay đổi khi bạn tải lại trang" + default: "(mặc định)" + password_confirmation: + title: "Nhập lại Password" + last_posted: "Bài viết cuối cùng" + last_emailed: "Đã email lần cuối" + last_seen: "được thấy" + created: "Đã tham gia" + log_out: "Log Out" + location: "Vị trí" + card_badge: + title: "Huy hiệu của thẻ thành viên" + website: "Web Site" + email_settings: "Email" + email_digests: + title: "Khi tôi không truy cập, gửi email gợi ý những cái mới cho tôi:" + daily: "hàng ngày" + every_three_days: "ba ngày một" + weekly: "hàng tuần" + every_two_weeks: "hai tuần một" + email_direct: "Gửi cho tôi một email khi có người trích dẫn, trả lời cho bài viết của tôi, đề cập đến @username của tôi, hoặc mời tôi đến một chủ đề" + email_private_messages: "Gửi cho tôi email khi có ai đó nhắn tin cho tôi" + email_always: "Gửi email thông báo cho tôi mỗi khi tôi kích hoạt trên website này" + other_settings: "Khác" + categories_settings: "Chuyên mục" + new_topic_duration: + label: "Để ý tới chủ đề mới khi" + not_viewed: "Tôi chưa từng xem họ" + last_here: "tạo ra kể từ lần cuối tôi ở đây" + after_1_day: "được tạo ngày hôm qua" + after_2_days: "được tạo 2 ngày trước" + after_1_week: "được tạo tuần trước" + after_2_weeks: "được tạo 2 tuần trước" + auto_track_topics: "Tự động theo dõi các chủ đề tôi tạo" + auto_track_options: + never: "không bao giờ" + immediately: "ngay lập tức" + after_30_seconds: "sau 30 giây" + after_1_minute: "sau 1 phút" + after_2_minutes: "sau 2 phút" + after_3_minutes: "sau 3 phút" + after_4_minutes: "sau 4 phút" + after_5_minutes: "sau 5 phút" + after_10_minutes: "sau 10 phút" + invited: + search: "gõ để tìm kiếm thư mời " + title: "Lời mời" + user: "User được mời" + sent: "Đã gửi" + redeemed: "Lời mời bù lại" + redeemed_tab: "Làm lại" + redeemed_tab_with_count: "Làm lại ({{count}})" + redeemed_at: "Nhận giải" + pending: "Lời mời tạm hoãn" + pending_tab: "Đang treo" + pending_tab_with_count: "Đang xử lý ({{count}})" + topics_entered: "Bài viết được xem " + posts_read_count: "Đọc bài viết" + expired: "Thư mời này đã hết hạn." + rescind: "Xoá" + rescinded: "Lời mời bị xóa" + reinvite: "Mời lại" + reinvited: "Gửi lại lời mời" + time_read: "Đọc thời gian" + days_visited: "Số ngày đã thăm" + account_age_days: "Thời gian của tài khoản theo ngày" + create: "Gửi một lời mời" + generate_link: "Chép liên kết Mời" + bulk_invite: + none: "Bạn đã mời ai ở đây chưa. Bạn có thể mời một hoặc một nhóm bằng tải lên hàng loạt file mời." + text: "Mời hàng loạt bằng file" + uploading: "Uploading..." + success: "Tải lên thành công, bạn sẽ được thông báo qua tin nhắn khi quá trình hoàn tất." + error: "Có lỗi xảy ra khi upload '{{filename}}': {{message}}" + password: + title: "Mật khẩu" + too_short: "Mật khẩu của bạn quá ngắn." + common: "Mật khẩu quá đơn giản, rất dễ bị đoán ra" + same_as_username: "Mật khẩu của bạn trùng với tên đăng nhập." + same_as_email: "Mật khẩu của bạn trùng với email của bạn." + ok: "Mật khẩu của bạn có vẻ ổn." + instructions: "Ít nhất %{count} ký tự" + associated_accounts: "Đăng nhập" + ip_address: + title: "Địa chỉ IP cuối cùng" + registration_ip_address: + title: "Địa chỉ IP đăng ký" + avatar: + title: "Ảnh đại diện" + header_title: "hồ sơ cá nhân, tin nhắn, đánh dấu và sở thích" + title: + title: "Tiêu đề" + filters: + all: "All" + stream: + posted_by: "Đăng bởi" + sent_by: "Gửi bởi" + private_message: "tin nhắn" + the_topic: "chủ đề" + loading: "Đang tải..." + errors: + prev_page: "trong khi cố gắng để tải" + reasons: + network: "Mạng Internet bị lỗi" + server: "Máy chủ đang có vấn đề" + forbidden: "Bạn không thể xem được" + unknown: "Lỗi" + not_found: "Không Tìm Thấy Trang" + desc: + network: "Hãy kiểm tra kết nối của bạn" + network_fixed: "Hình như nó trở lại." + server: "Mã lỗi : {{status}}" + forbidden: "Bạn không được cho phép để xem mục này" + unknown: "Có một lỗi gì đó đang xảy ra" + buttons: + back: "Quay trở lại" + again: "Thử lại" + fixed: "Load lại trang" + close: "Đóng lại" + assets_changed_confirm: "Website đã được cập nhật bản mới. Bạn có thể làm mới lại trang để có thể sử dụng bản mới được cập nhật" + logout: "Bạn đã đăng xuất" + refresh: "Tải lại" + read_only_mode: + enabled: "Chế độ chỉ đọc được kích hoạt. Bạn có thể tiếp tục duyệt tới trang web, nhưng các tương tác có thể không hoạt động." + login_disabled: "Chức năng Đăng nhập đã bị tắt khi website trong trạng thái chỉ đọc" + learn_more: "tìm hiểu thêm..." + year: 'năm' + year_desc: 'chủ đề được tạo ra trong 365 ngày qua' + month: 'tháng' + month_desc: 'chủ đề được tạo ra trong 30 ngày qua' + week: 'tuần' + week_desc: 'chủ đề được tạo ra trong 7 ngày qua' + day: 'ngày' + first_post: Bài viết đầu tiên + mute: Im lặng + unmute: Bỏ im lặng + last_post: Bài viết cuối cùng + last_reply_lowercase: trả lời cuối cùng + replies_lowercase: + other: trả lời + signup_cta: + sign_up: "Đăng ký" + hide_session: "Nhắc vào ngày mai" + hide_forever: "không, cảm ơn" + summary: + enabled_description: "Bạn đang xem một bản tóm tắt của chủ đề này: các bài viết thú vị nhất được xác định bởi cộng đồng." + description: "Có {{count}} trả lời" + description_time: "Đây là {{count}} trả lời tương ứng với thời gian đọc {{readingTime}} phút." + enable: 'Tóm tắt lại chủ đề' + disable: 'HIển thị tất cả các bài viết' + deleted_filter: + enabled_description: "Chủ để này có chứa các bài viết bị xoá, chúng đã bị ẩn đi" + disabled_description: "Xoá các bài viết trong các chủ để được hiển thị" + enable: "Ẩn các bài viết bị xoá" + disable: "Xem các bài viết bị xoá" + private_message_info: + title: "Tin nhắn" + invite: "Mời người khác..." + remove_allowed_user: "Bạn thực sự muốn xóa {{name}} từ tin nhắn này?" + email: 'Email' + username: 'Username' + last_seen: 'Đã xem' + created: 'Tạo bởi' + created_lowercase: 'ngày tạo' + trust_level: 'Độ tin tưởng' + search_hint: 'username, email or IP address' + create_account: + title: "Tạo tài khoản mới" + failed: "Có gì đó không đúng, có thể email này đã được đăng ký, thử liên kết quên mật khẩu" + forgot_password: + title: "Đặt lại mật khẩu" + action: "Tôi đã quên mật khẩu của tôi" + invite: "Điền vào username của bạn hoặc địa chỉ email và chúng tôi sẽ gửi bạn email để khởi tạo lại mật khẩu" + reset: "Tạo lại mật khẩu" + complete_username: "Nếu một tài khoản phù hợp với tên thành viên % {username} , bạn sẽ nhận được một email với hướng dẫn về cách đặt lại mật khẩu của bạn trong thời gian ngắn." + complete_email: "Nếu một trận đấu tài khoản % {email} , bạn sẽ nhận được một email với hướng dẫn về cách đặt lại mật khẩu của bạn trong thời gian ngắn." + complete_username_found: "Chúng tôi tìm thấy một tài khoản phù hợp với tên thành viên % {username} , bạn sẽ nhận được một email với hướng dẫn về cách đặt lại mật khẩu của bạn trong thời gian ngắn." + complete_email_found: "Chúng tôi tìm thấy một tài khoản phù hợp với % {email} , bạn sẽ nhận được một email với hướng dẫn về cách đặt lại mật khẩu của bạn trong thời gian ngắn." + complete_username_not_found: "Không có tài khoản phù hợp với tên thành viên % {username} " + complete_email_not_found: "Không tìm thấy tài khoản nào tương ứng với %{email}" + login: + title: "Đăng nhập" + username: "Thành viên" + password: "Mật khẩu" + email_placeholder: "Email hoặc tên đăng nhập " + caps_lock_warning: "Phím Caps Lock đang được bật" + error: "Không xác định được lỗi" + rate_limit: "Xin đợi trước khi đăng nhập lại lần nữa." + blank_username_or_password: "Bạn phải nhập email hoặc username, và mật khẩu" + reset_password: 'Khởi tạo mật khẩu' + logging_in: "Đăng nhập..." + or: "Hoặc" + authenticating: "Đang xác thực..." + awaiting_confirmation: "Tài khoản của bạn đang đợi kích hoạt, sử dụng liên kết quên mật khẩu trong trường hợp kích hoạt ở 1 email khác." + awaiting_approval: "Tài khoản của bạn chưa được chấp nhận bới thành viên. Bạn sẽ được gửi một email khi được chấp thuận " + requires_invite: "Xin lỗi, bạn phải được mời để tham gia diễn đàn" + not_activated: "Bạn không thể đăng nhập. Chúng tôi đã gửi trước email kích hoạt cho bạn tại {{sentTo}}. Vui lòng làm theo hướng dẫn trong email để kích hoạt tài khoản của bạn." + not_allowed_from_ip_address: "Bạn không thể đăng nhập từ địa chỉ IP này" + admin_not_allowed_from_ip_address: "Bạn không thể đăng nhập với quyền quản trị từ địa chỉ IP đó." + resend_activation_email: "Bấm đây để gửi lại email kích hoạt" + sent_activation_email_again: "Chúng tôi gửi email kích hoạt tới cho bạn ở {{currentEmail}}. Nó sẽ mất vài phút để đến; bạn nhớ check cả hồm thư spam nhe. " + to_continue: "Vui lòng đăng nhập" + google: + title: "với Google " + message: "Chứng thực với Google (Bạn hãy chắc chắn là chặn popup không bật)" + google_oauth2: + title: "với Google" + message: "Chứng thực với Google (chắc chắn rằng cửa sổ pop up blocker không được kích hoạt)" + twitter: + title: "với Twitter" + message: "Chứng thực với Twitter(hãy chắc chắn là chăn pop up không bật)" + facebook: + title: "với Facebook" + message: "Chứng thực với Facebook(chắc chắn là chặn pop up không bật)" + yahoo: + title: "với Yahoo" + message: "Chứng thực với Yahoo (Chắc chắn chặn pop up không bật)" + github: + title: "với GitHub" + message: "Chứng thực với GitHub (chắc chắn chặn popup không bật)" + apple_international: "Apple/International" + google: "Google" + twitter: "Twitter" + emoji_one: "Emoji One" + composer: + emoji: "Emoji :smile:" + options: "Lựa chọn" + whisper: "nói chuyện" + add_warning: "Đây là một cảnh báo chính thức" + toggle_whisper: "Chuyển chế độ Nói chuyện" + posting_not_on_topic: "Bài viết nào bạn muốn trả lời " + saving_draft_tip: "đang lưu..." + saved_draft_tip: "Đã lưu" + saved_local_draft_tip: "Đã lưu locally" + similar_topics: "Bài viết của bạn tương tự với " + drafts_offline: "Nháp offline" + error: + title_missing: "Tiêu đề là bắt buộc" + title_too_short: "Tiêu để phải có ít nhất {{min}} ký tự" + title_too_long: "Tiêu đề có tối đa {{max}} ký tự" + post_missing: "Bài viết không được bỏ trắng" + post_length: "Bài viết phải có ít nhất {{min}} ký tự" + try_like: 'Các bạn đã thử các nút ?' + category_missing: "Bạn phải chọn một phân loại" + save_edit: "Lưu chỉnh sửa" + reply_original: "Trả lời cho bài viết gốc" + reply_here: "Trả lời đây " + reply: "Trả lời " + cancel: "Huỷ" + create_topic: "Tạo chủ đề" + create_pm: "Tin nhắn" + title: "Hoặc nhất Ctrl+Enter" + users_placeholder: "Thêm thành viên " + title_placeholder: "Tóm tắt lại thảo luận này trong một câu ngắn gọn" + edit_reason_placeholder: "Tại sao bạn sửa" + show_edit_reason: "(thêm lý do sửa)" + reply_placeholder: "Gõ ở đây. Sử dụng Markdown, BBCode, hoặc HTML để định dạng. Kéo hoặc dán ảnh." + view_new_post: "Xem bài đăng mới của bạn. " + saved: "Đã lưu" + saved_draft: "Bài nháp đang lưu. Chọn để tiếp tục." + uploading: "Đang đăng " + show_preview: 'Xem trước »' + hide_preview: '«ẩn xem trước' + quote_post_title: "Trích dẫn cả bài viết" + bold_title: "In đậm" + bold_text: "chữ in đậm" + italic_title: "Nhấn mạnh" + italic_text: "văn bản nhấn mạnh" + link_title: "Liên kết" + link_description: "Nhập mô tả liên kết ở đây" + link_dialog_title: "Chèn liên kết" + link_optional_text: "tiêu đề tùy chọn" + quote_title: "Trích dẫn" + quote_text: "Trích dẫn" + code_title: "Văn bản định dạng trước" + code_text: "lùi đầu dòng bằng 4 dấu cách" + upload_title: "Tải lên" + upload_description: "Nhập mô tả tải lên ở đây" + olist_title: "Danh sách kiểu số" + ulist_title: "Danh sách kiểu ký hiệu" + list_item: "Danh sách các mục" + heading_title: "Tiêu đề" + heading_text: "Tiêu đề" + hr_title: "Căn ngang" + help: "Trợ giúp soạn thảo bằng Markdown" + toggler: "ẩn hoặc hiển thị bảng điều khiển soạn thảo" + modal_cancel: "Hủy" + admin_options_title: "Tùy chọn quản trị viên cho chủ đề này" + auto_close: + label: "Thời gian tự khóa chủ đề:" + error: "Vui lòng nhập một giá trị hợp lệ." + based_on_last_post: "Không đóng cho đến khi bài viết cuối cùng trong chủ đề này trở thành bài cũ" + all: + examples: 'Nhập giờ (định dạng 24h), thời gian chính xác ( vd: 17:30) hoặc thời gian kèm ngày tháng (2013-11-22 14:00).' + limited: + units: "(# của giờ)" + examples: 'Nhập số giờ ( theo định dạng 24h)' + notifications: + title: "thông báo của @name nhắc đến, trả lời bài của bạn và chủ đề, tin nhắn, vv" + none: "Không thể tải các thông báo tại thời điểm này." + more: "xem thông báo cũ hơn" + total_flagged: "tổng số bài viết gắn cờ" + mentioned: "

{{username}} {{description}}

" + quoted: "

{{username}} {{description}}

" + replied: "

{{username}} {{description}}

" + posted: "

{{username}} {{description}}

" + edited: "

{{username}} {{description}}

" + liked: "

{{username}} {{description}}

" + private_message: "

{{username}} {{description}}

" + invited_to_private_message: "

{{username}} {{description}}

" + invited_to_topic: "

{{username}} {{description}}

" + invitee_accepted: "

{{username}} chấp nhận lời mời của bạn

" + moved_post: "

{{username}} chuyển {{description}}

" + linked: "

{{username}} {{description}}

" + granted_badge: "

Thu được '{{description}}'

" + alt: + mentioned: "Được nhắc đến bởi" + quoted: "Trích dẫn bởi" + replied: "Đã trả lời" + posted: "Đăng bởi" + edited: "Bài viết của bạn được sửa bởi" + liked: "Bạn đã like bài viết" + private_message: "Tin nhắn riêng từ" + invitee_accepted: "Lời mời được chấp nhận bởi" + moved_post: "Bài viết của bạn đã được di chuyển bởi" + linked: "Liên kết đến bài viết của bạn" + popup: + mentioned: '{{username}} nhắc đến bạn trong "{{topic}}" - {{site_title}}' + quoted: '{{username}} trích lời bạn trong "{{topic}}" - {{site_title}}' + replied: '{{username}} trả lời cho bạn trong "{{topic}}" - {{site_title}}' + posted: '{{username}} gửi bài trong "{{topic}}" - {{site_title}}' + private_message: '{{username}} đã gửi cho bạn một tin nhắn trong "{{topic}}" - {{site_title}}' + linked: '{{username}} liên quan đến bài viết của bạn từ "{{topic}}" - {{site_title}}' + upload_selector: + title: "Thêm một ảnh" + title_with_attachments: "Thêm một ảnh hoặc tệp tin" + from_my_computer: "Từ thiết bị của tôi" + from_the_web: "Từ Web" + remote_tip: "đường dẫn tới hình ảnh" + local_tip: "chọn hình từ thiết bị của bạn" + hint: "(Bạn cũng có thể kéo & thả vào trình soạn thảo để tải chúng lên)" + hint_for_supported_browsers: "bạn có thể kéo và thả ảnh vào trình soan thảo này" + uploading: "Đang tải lên" + select_file: "Chọn Tài liệu" + image_link: "liên kết hình ảnh của bạn sẽ trỏ đến" + search: + sort_by: "Sắp xếp theo" + relevance: "Độ phù hợp" + latest_post: "Bài viết mới nhất" + most_viewed: "Xem nhiều nhất" + most_liked: "Like nhiều nhất" + select_all: "Chọn tất cả" + clear_all: "Xóa tất cả" + result_count: + other: "{{count}} kết quả cho \"{{term}}\"" + title: "tìm kiếm chủ đề, bài viết, tài khoản hoặc các danh mục" + no_results: "Không tìm thấy kết quả." + no_more_results: "Không tìm thấy kết quả" + search_help: Giúp đỡ tìm kiếm + searching: "Đang tìm ..." + post_format: "#{{post_number}} bởi {{username}}" + context: + user: "Tìm bài viết của @{{username}}" + category: "Tìm danh mục \"{{category}}\"" + topic: "Tìm trong chủ đề này" + private_messages: "Tìm tin nhắn" + hamburger_menu: "đi đến danh sách chủ đề hoặc danh mục khác" + new_item: "mới" + go_back: 'quay trở lại' + not_logged_in_user: 'Trang cá nhân với tóm tắt các hoạt động và cấu hình' + current_user: 'đi đến trang cá nhân của bạn' + topics: + bulk: + reset_read: "Đặt lại lượt đọc" + delete: "Xóa chủ đề" + dismiss_new: "Bỏ " + change_category: "Chuyển chuyên mục" + close_topics: "Đóng các chủ đề" + archive_topics: "Chủ đề Lưu trữ" + notification_level: "Thay đổi cấp độ thông báo" + choose_new_category: "Chọn chuyên mục mới cho chủ đề này:" + selected: + other: "Bạn đã chọn {{count}} chủ đề" + none: + unread: "Bạn không có chủ đề nào chưa đọc." + new: "Bạn không có chủ đề mới nào." + read: "Bạn vẫn chưa đọc bất kì chủ đề nào." + posted: "Bạn vẫn chưa đăng bài trong bất kì một chủ đề nào" + latest: "Chán quá. Chẳng có chủ đề mới nào hết trơn." + hot: "Không có chủ đề nào nổi bật." + bookmarks: "Bạn chưa chủ đề nào được đánh dấu." + category: "Không có chủ đề nào trong {{category}} ." + top: "Không có chủ đề top." + search: "Không có kết quả tìm kiếm." + educate: + new: '

Chủ đề mới của bạn nằm ở đây.

Mặc định, chủ đề được coi là mới và sẽ hiện một chỉ báo new nếu chúng được tạo trong 2 ngày vừa qua.

Bạn có thể thay đổi cái này trong preferences.

' + unread: '

Những chủ đề chưa đọc của bạn nằm ở đây.

Mặc định, chủ đề được coi là chưa đọc và sẽ hiện số lượng chưa đọc 1 nếu bạn:

  • Đã tạo chủ đề
  • Đã phản hồi chủ đề
  • Đọc chủ đề lâu hơn 4 phút

Hoặc nếu bạn đặt chủ đề là Đã theo dấu hoặc Đã xem qua điều khiển thông bao tại cuối mỗi chủ đề.

Bạn có thể thay đổi điều này trong preferences.

' + bottom: + latest: "Không còn thêm chủ đề nào nữa." + hot: "Không còn của đề nổi bật nào nữa." + posted: "Ở đây không có thêm chủ đề nào được đăng." + read: "Không còn thêm chủ đề chưa đọc nào nữa." + new: "Không còn thêm chủ đề mới nào nữa." + unread: "Không còn thêm chủ đề chưa đọc nào nữa." + category: "Không còn thêm chủ đề nào trong {{category}} ." + top: "Không còn của đề top nào nữa." + bookmarks: "Không còn thêm chủ đề được đánh dấu nào nữa." + search: "Không có thêm kết quả tìm kiếm nào nữa." + topic: + unsubscribe: + stop_notifications: "Từ bây giờ bạn sẽ không nhận thông báo từ {{title}}" + change_notification_state: "Tình trạn thông báo của bạn là" + filter_to: "{{post_count}} bài đăng trong chủ đề" + create: 'Chủ đề Mới' + create_long: 'Tạo một Chủ đề mới' + private_message: 'Bắt đầu một thông điệp' + list: 'Chủ đề' + new: 'chủ đề mới' + unread: 'chưa đọc' + new_topics: + other: '{{count}} chủ đề mới.' + unread_topics: + other: '{{count}} chủ đề chưa đọc.' + title: 'Chủ đề' + invalid_access: + title: "Chủ đề này là riêng tư" + description: "Xin lỗi, bạn không có quyền truy cập vào chủ đề đó!" + login_required: "Bạn cần phải đăng nhập để xem chủ đề đó" + server_error: + title: "Tải chủ đề thất bại" + description: "Xin lỗi, chúng tôi không thể tải chủ đề, có thể do kết nối có vấn đề. Xin hãy thử lại. Nếu vấn đề còn xuất hiện, hãy cho chúng tôi biết" + not_found: + title: "Không tìm thấy chủ đề" + description: "Xin lỗi, chúng tôi không thể tìm thấy chủ đề đó. Có lẽ nó đã bị loại bởi mod?" + total_unread_posts: + other: "Bạn có {{number}} bài đăng chưa đọc trong chủ đề này" + unread_posts: + other: "bạn có {{number}} bài đăng củ chưa đọc trong chủ đề này" + new_posts: + other: "có {{count}} bài đăng mới trong chủ đề này từ lần đọc cuối" + likes: + other: "có {{count}} thích trong chủ để này" + back_to_list: "Quay lại danh sách chủ đề" + options: "Các lựa chọn chủ đề" + show_links: "Hiển thị liên kết trong chủ đề này" + toggle_information: "chuyển đổi các chi tiết chủ để" + read_more_in_category: "Muốn đọc nữa? Xem qua các chủ đề khác trong {{catLink}} hoặc {{latestLink}}" + read_more: "Muốn đọc nữa? {{catLink}} hoặc {{latestLink}}" + read_more_MF: "Có { UNREAD, plural, =0 {} one { is 1 unread } other { are # unread } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {is } other{}} 1 new topic} other { {BOTH, select, true{and } false {are } other{}} # new topics} } remaining, or {CATEGORY, select, true {browse other topics in {catLink}} false {{latestLink}} other {}}" + browse_all_categories: Duyệt tất cả các hạng mục + view_latest_topics: xem các chủ đề mới nhất + suggest_create_topic: Tại sao không tạo một chủ đề mới? + jump_reply_up: nhảy đến những trả lời trước đó + jump_reply_down: nhảy tới những trả lời sau đó + deleted: "Chủ đề này đã bị xóa" + auto_close_notice: "Chủ đề này sẽ tự động đóng %{timeLeft}." + auto_close_notice_based_on_last_post: "Chủ đề này sẽ đóng %{duration} sau trả lời cuối cùng." + auto_close_title: 'Tự động-Đóng các Cài đặt' + auto_close_save: "Lưu" + auto_close_remove: "Đừng Tự Động-Đóng Chủ Đề Này" + progress: + title: tiến trình của chủ đề + go_top: "trên cùng" + go_bottom: "dưới cùng" + go: "đi tới" + jump_bottom: "nhảy tới bài viết cuối cùng" + jump_bottom_with_number: "nhảy tới bài viết %{post_number}" + total: tổng số bài viết + current: bài viết hiện tại + position: "bài viết %{current} trong %{total}" + notifications: + reasons: + '3_6': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang xem chuyên mục nàyotification' + '3_5': 'Bạn sẽ nhận được các thông báo bởi vì bạn đã bắt đầu xem chủ đề này một cách tự động' + '3_2': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang xem chủ đề này' + '3_1': 'Bạn sẽ được nhận thông báo bởi bạn đã tạo chủ để này.' + '3': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang xem chủ đề này' + '2_8': 'Bạn sẽ nhận được thông báo bởi vì bạn đang theo dõi chuyên mục này.' + '2_4': 'Bạn sẽ nhận được các thông báo bởi vì bạn đã đăng một trả lời vào chủ đề này' + '2_2': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang theo dõi chủ đề này.' + '2': 'Bạn sẽ nhận được các thông báo bởi vì bạn đọc chủ đề này ' + '1_2': 'Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn' + '1': 'Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn' + '0_7': 'Bạn đang bỏ qua tất cả các thông báo trong chuyên mục này' + '0_2': 'Bạn đang bỏ qua tất cả các thông báo trong chủ đề này' + '0': 'Bạn đang bỏ qua tất cả các thông báo trong chủ đề này' + watching_pm: + title: "Đang xem" + description: "Bạn sẽ được thông báo về từng trả lời mới trong tin nhắn này, và một số trả lời mới sẽ được hiển thị" + watching: + title: "Dang theo dõi" + description: "Bạn sẽ được thông báo về từng trả lời mới trong tin nhắn này, và một số trả lời mới sẽ được hiển thị" + tracking_pm: + title: "Đang theo dõi" + description: "Một số trả lời mới sẽ được hiển thị trong tin nhắn này. Bạn sẽ được thông báo nếu ai đó đề cập đến @tên của bạn hoặc trả lời bạn" + tracking: + title: "Đang theo dõi" + description: "Một số trả lời mới sẽ được hiển thị trong chủ đề này. Bạn sẽ được thông báo nếu ai đó đề cập đến @tên của bạn hoặc trả lời bạn" + regular: + title: "Bình thường" + regular_pm: + title: "Bình thường" + muted_pm: + title: "Im lặng" + description: "Bạn sẽ không bao giờ được thông báo về bất cứ điều gì về tin nhắn này. " + muted: + title: "Im lặng" + actions: + recover: "Không-Xóa Chủ Đề Này" + delete: "Xóa-Chủ Đề Này" + open: "Mở Chủ Đề" + close: "Đóng Chủ Đề" + multi_select: "Chọn Bài Viết..." + auto_close: "Tự Động Đóng..." + pin: "Ghim Chủ Đề..." + unpin: "Bỏ-Ghim Chủ Đề..." + unarchive: "Chủ đề Không Lưu Trữ" + archive: "Chủ Đề Lưu Trữ" + reset_read: "Đặt lại dữ liệu đọc" + feature: + pin: "Ghim Chủ Đề" + unpin: "Bỏ-Ghim Chủ Đề" + pin_globally: "Ghim Chủ Đề Tổng Thể" + make_banner: "Banner chủ đề" + remove_banner: "Bỏ banner chủ đề" + reply: + title: 'Trả lời' + help: 'bắt đầu soạn một trả lời mới cho chủ đề này' + clear_pin: + title: "Xóa ghim" + help: "Xóa trạng thái ghim của chủ đề này để nó không còn xuất hiện trên cùng danh sách chủ đề của bạn" + share: + title: 'Chia sẻ' + help: 'Chia sẻ một liên kết đến chủ đề này' + flag_topic: + title: 'Gắn cờ' + help: 'đánh dấu riêng tư chủ đề này cho sự chú ý hoặc gửi một thông báo riêng về nó' + success_message: 'Bạn đã đánh dấu thành công chủ đề này' + feature_topic: + confirm_pin: "Bạn đã có {{count}} chủ đề được ghim. Qúa nhiều chủ đề được ghim có thể là một trở ngại cho những thành viên mới và thành viên ẩn danh. Bạn có chắc chắn muốn ghim chủ đề khác trong chuyên mục này?" + unpin: "Xóa chủ đề này từ phần trên cùng của chủ đề {{categoryLink}}" + pin_note: "Người dùng có thể bỏ ghim chủ đề riêng cho mình" + pin_validation: "Ngày được yêu câu để gắn chủ đề này" + unpin_globally: "Bỏ chủ đề này khỏi phần trên cùng của danh sách tất cả các chủ đề" + global_pin_note: "Người dùng có thể bỏ ghim chủ đề riêng cho mình" + inviting: "Đang mời..." + invite_private: + email_or_username_placeholder: "địa chỉ thư điện tử hoặc tên người dùng" + action: "Mời" + error: "Xin lỗi, có lỗi khi mời người dùng này." + group_name: "Nhóm tên" + invite_reply: + title: 'Mời' + username_placeholder: "tên người dùng" + action: 'Gửi Lời Mời' + to_forum: "Chúng tôi sẽ gửi một email tóm tắt cho phép bạn của bạn gia nhập trực tiệp bằng cách nhấp chuột vào một đường dẫn, không cần phải đăng nhập." + sso_enabled: "Nhập tên đăng nhập hoặc địa chỉ email của người mà bạn muốn mời vào chủ đề này." + to_topic_blank: "Nhập tên đăng nhập hoặc địa chỉ email của người bạn muốn mời đến chủ đề này." + email_placeholder: 'name@example.com' + login_reply: 'Đăng nhập để trả lời' + filters: + n_posts: + other: "{{count}} bài viết" + cancel: "Bỏ đièu kiện lọc" + split_topic: + title: "Di chuyển tới Chủ đề mới" + action: "di chuyển tới chủ đề mới" + topic_name: "Tên chủ đề mới" + error: "Có lỗi khi di chuyển bài viết tới chủ đề mới." + merge_topic: + title: "Di chuyển tới chủ đề đang tồn tại" + action: "di chuyển tới chủ đề đang tồn tại" + error: "Có lỗi khi di chuyển bài viết đến chủ đề này." + change_owner: + title: "Chuyển chủ sở hữu bài viết" + action: "chuyển chủ sở hữu" + label: "Chủ sở hữ mới của Bài viết" + placeholder: "tên đăng nhập của chủ sở hữu mới" + change_timestamp: + title: "Đổi Timestamp" + action: "đổi timestamp" + invalid_timestamp: "Timestamp không thể trong tương lai." + error: "Có lỗi khi thay đổi timestamp của chủ đề." + multi_select: + select: 'chọn' + selected: 'đã chọn ({{count}})' + select_replies: 'chọn + trả lời' + delete: xóa lựa chọn + cancel: hủy lựa chọn + select_all: chọn tất cả + deselect_all: bỏ chọn tất cả + description: + other: Bạn đã chọn {{count}} bài viết. + post: + reply: " {{replyAvatar}} {{usernameLink}}" + reply_topic: " {{link}}" + quote_reply: "trả lời trích dẫn" + edit: "Đang sửa {{link}} {{replyAvatar}} {{username}}" + edit_reason: "Lý do: " + post_number: "bài viết {{number}}" + last_edited_on: "đã sửa bài viết lần cuối lúc" + reply_as_new_topic: "Trả lời như là liên kết đến Chủ đề" + continue_discussion: "Tiếp tục thảo luận từ {{postLink}}:" + follow_quote: "đến bài viết trích dẫn" + show_full: "Hiển thị đầy đủ bài viết" + show_hidden: 'Xem nội dung ẩn' + expand_collapse: "mở/đóng" + gap: + other: "xem {{count}} trả lời bị ẩn" + more_links: "hơn {{count}}..." + unread: "Bài viết chưa đọc" + has_replies: + other: "{{count}} Trả lời" + has_likes: + other: "{{count}} Thích" + has_likes_title: + other: "{{count}} người thích bài viết này" + errors: + create: "Xin lỗi, có lỗi xảy ra khi tạo bài viết của bạn. Vui lòng thử lại." + edit: "Xin lỗi, có lỗi xảy ra khi sửa bài viết của bạn. Vui lòng thử lại." + upload: "Xin lỗi, có lỗi xảy ra khi tải lên tập tin này. Vui lòng thử lại." + attachment_too_large: "Xin lỗi, tập tin của bạn tải lên quá lớn (kích thước tối đa là {{max_size_kb}}kb)." + file_too_large: "Xin lỗi, tập tin của bạn tải lên quá lớn (kích thước tối đa là {{max_size_kb}}kb)" + too_many_uploads: "Xin lỗi, bạn chỉ có thể tải lên 1 file cùng 1 lúc." + too_many_dragged_and_dropped_files: "Xin lỗi, bạn chỉ có thể kéo và thả 10 tập tin cùng lúc." + upload_not_authorized: "Xin lỗi, tập tin của bạn tải lên chưa được cho phép (định dạng cho phép: {{authorized_extensions}})." + image_upload_not_allowed_for_new_user: "Xin lỗi, tài khoản mới không thể tải lên ảnh." + attachment_upload_not_allowed_for_new_user: "Xin lỗi, tài khoản mới không thể tải lên đính kèm." + attachment_download_requires_login: "Xin lỗi, bạn cần đăng nhập để tải về đính kèm." + abandon: + confirm: "Bạn có chắc muốn bỏ bài viết của bạn?" + no_value: "Không, giữ lại" + yes_value: "Đồng ý, bỏ" + via_email: "bài viết này đăng qua email" + whisper: "bài viết này là lời nhắn từ điều hành viên" + wiki: + about: "bài viết này là wiki; người dùng cơ bản có thể sửa nó" + archetypes: + save: 'Lưu lựa chọn' + controls: + reply: "bắt đầu soản trả lời cho bài viết này" + like: "like bài viết này" + has_liked: "bạn đã like bài viết này" + undo_like: "hủy like" + edit: "sửa bài viết này" + edit_anonymous: "Xin lỗi, nhưng bạn cần đăng nhập để sửa bài viết này." + delete: "xóa bài viết này" + undelete: "hủy xóa bài viết này" + share: "chia sẻ liên kết đến bài viết này" + more: "Thêm" + delete_replies: + confirm: + other: "Bạn muốn xóa {{count}} trả lời cho bài viết này?" + yes_value: "Đồng ý, xóa những trả lời" + no_value: "Không, chỉ xóa chủ đề" + wiki: "Tạo Wiki" + unwiki: "Xóa Wiki" + convert_to_moderator: "Thêm màu Nhân viên" + revert_to_regular: "Xóa màu Nhân viên" + rebake: "Tạo lại HTML" + unhide: "Bỏ ẩn" + actions: + flag: 'Gắn cờ' + it_too: + off_topic: "Gắn cờ nó" + spam: "Gắn cờ nó" + inappropriate: "Gắn cờ nó" + custom_flag: "Gắn cờ nó" + bookmark: "Đánh dấu nó" + like: "Thích nó" + vote: "Bịnh chọn nó" + undo: + off_topic: "Hủy gắn cờ" + spam: "Hủy gắn cờ" + inappropriate: "Hủy gắn cờ" + bookmark: "Hủy đánh dấu" + like: "Hủy like" + vote: "Hủy bình chọn" + people: + off_topic: "{{icons}} đánh dấu nói sai chủ đề" + spam: "{{icons}} đánh dấu nó là rác" + spam_with_url: "{{icons}} gắn cờ cái này là rác" + inappropriate: "{{icons}} gắn cờ là không phù hợp" + notify_moderators: "{{icons}} thông báo cho điều hành viên" + notify_moderators_with_url: "{{icons}} đã thông báo quản trị viên" + notify_user: "{{icons}} gửi một tin nhắn" + notify_user_with_url: "{{icons}} gửi một tin nhắn" + bookmark: "{{icons}} đã đánh dấu" + like: "{{icons}} thích cái này" + vote: "{{icons}} bình chọn cho cái này" + by_you: + off_topic: "Bạn đã đánh dấu cái nfay là chủ đề đóng" + spam: "Bạn đã đánh dấu cái này là rác" + inappropriate: "Bạn đã đánh dấu cái này là không phù hợp" + notify_moderators: "Bạn đã đánh dấu cái này cho điều tiết" + notify_user: "Bạn đã gửi một tin nhắn đến người dùng này" + bookmark: "Bạn đã đánh dấu bài viết này" + like: "Bạn đã thích cái này" + vote: "Bạn đã bình chọn cho bài viết này" + by_you_and_others: + off_topic: + other: "Bạn và {{count}} người khác đã đánh dấu đây là chủ đề đóng" + spam: + other: "Bạn và {{count}} người khác gắn cờ nó là rác" + inappropriate: + other: "Bạn và {{count}} other người khác đã đánh dấu nó là không phù hợp" + notify_moderators: + other: "Bạn và {{count}} người khác gắn cờ nó là điều tiết" + notify_user: + other: "Bạn và {{count}} người khác đã gửi một tin nhắn đến người dùng này" + bookmark: + other: "Bạn và {{count}} người khác đã đánh dấu bài viết này" + like: + other: "Bạn và {{count}} người khác đã thích cái này" + vote: + other: "Bạn và {{count}} nười khác đã bình chọn cho bài viết này" + by_others: + off_topic: + other: "{{count}} người đã đánh dấu nó là chủ đề đóng" + spam: + other: "{{count}} người khác đánh dấu là rác" + inappropriate: + other: "{{count}} người khác đã đánh dấu là không phù hợp" + notify_user: + other: "{{count}} gửi tin nhắn đến người dùng này" + bookmark: + other: "{{count}} người đã đánh dấu bài viết này" + like: + other: "{count}} người đã thích cái này" + vote: + other: "{{count}} người đã bình chọn cho bài viết này" + delete: + confirm: + other: "Bạn muốn xóa những bài viết này?" + revisions: + controls: + first: "Sửa đổi đầu tiên" + previous: "Sửa đổi trước" + next: "Sửa đổi tiếp theo" + last: "Sửa đổi gần nhất" + hide: "Ẩn sửa đổi" + show: "Hiện sửa đổi" + displays: + inline: + button: ' HTML' + side_by_side: + button: ' HTML' + side_by_side_markdown: + button: ' Thô' + category: + can: 'can…' + none: '(không danh mục)' + all: 'Tất cả danh mục' + edit: 'sửa' + edit_long: "Sửa" + view: 'Xem Chủ đề trong Danh mục' + general: 'Chung' + settings: 'Cấu hình' + topic_template: "Mẫu Chủ đề" + delete: 'Xóa chuyên mục' + create: 'Chuyên mục mới' + create_long: 'Tạo Chủ đề mới' + save: 'Lưu chuyên mục' + creation_error: Có lỗi xảy ra khi tạo chuyên mục + save_error: Có lỗi xảy ra khi lưu chuyên mục + name: "Tên chuyên mục" + description: "Mô tả" + topic: "chủ đề chuyên mục" + logo: "Logo của chuyên mục" + background_image: "Ảnh nền của chuyên mục" + background_color: "Màu nền" + name_placeholder: "Tối đa một hoặc hai từ" + color_placeholder: "Bất cứ màu nào" + delete_confirm: "Bạn có chắc sẽ xóa chuyên mục này chứ?" + delete_error: "Có lỗi xảy ra khi xóa chuyên mục này" + list: "Danh sách chuyên mục" + no_description: "Hãy thêm mô tả cho chuyên mục này" + change_in_category_topic: "Sửa mô tả" + already_used: 'Màu này đã được dùng bởi chuyên mục khác' + security: "Bảo mật" + images: "Hình ảnh" + auto_close_label: "Tự động khóa chủ đề sau:" + auto_close_units: "giờ" + email_in: "Tùy chỉnh địa chỉ nhận thư điện tử " + email_in_allow_strangers: "Nhận thư điện tử từ người gửi vô danh không tài khoản" + email_in_disabled_click: 'kích hoạt thiết lập thư điện tử' + allow_badges_label: "Cho phép thưởng huy hiệu trong chuyên mục này" + edit_permissions: "Sửa quyền" + add_permission: "Thêm quyền" + this_year: "năm nay" + position: "vị trí" + default_position: "vị trí mặc định" + parent: "Danh mục cha" + notifications: + watching: + title: "Theo dõi" + tracking: + title: "Đang theo dõi" + regular: + title: "Bình thường" + muted: + title: "Im lặng" + flagging: + action: 'Đánh dấu Bài viết' + notify_action: 'Tin nhắn' + delete_spammer: "Xóa người Spam" + delete_confirm: "Bạn đang định xóa %{posts} bài đăng và %{topics} chủ đề từ người dùng này, loại tài khoản, ngăn đăng ký từ địa chỉ IP %{ip_address} của họ, và thêm địa chỉ email %{email} vào danh sách chặn vĩnh viễn. Bạn có chắc người dùng này thật sự là một spammer?" + ip_address_missing: "(N/A)" + hidden_email_address: "(ẩn)" + formatted_name: + off_topic: "Nó là sai chủ đề" + spam: "Nó là rác" + custom_message: + more: "còn {{n}}" + flagging_topic: + action: "Gắn cờ Chủ đề" + notify_action: "Tin nhắn" + topic_map: + title: "Tóm tắt Chủ đề" + links_title: "Liên kết phổ biến" + clicks: + other: "%{count} nhấp chuột" + topic_statuses: + warning: + help: "Đây là một cảnh báo chính thức." + bookmarked: + help: "Bạn đã đánh dấu chủ đề này" + locked: + help: "Chủ đề đã đóng; không cho phép trả lời mới" + unpinned: + title: "Hủy gắn" + pinned: + title: "Gắn" + posts: "Bài viết" + posts_lowercase: "bài viết" + posts_long: "Có {{number}} bài đăng trong chủ đề này" + posts_likes_MF: | + Chủ đề này có {count, plural, one {1 reply} other {# replies}} {ratio, select, + low {with a high like to post ratio} + med {with a very high like to post ratio} + high {with an extremely high like to post ratio} + other {}} + original_post: "Bài viết gốc" + views: "Lượt xem" + views_lowercase: + other: "lượt xem" + replies: "Trả lời" + views_long: "chủ đề đã được xem {{number}} lần" + activity: "Hoạt động" + likes: "Lượt thích" + likes_lowercase: + other: "lượt thích" + likes_long: "Có {{number}} thích trong chủ đề này" + users: "Người dùng" + users_lowercase: + other: "người dùng" + category_title: "Danh mục" + history: "Lịch sử" + changed_by: "bởi {{author}}" + raw_email: + title: "Email gốc" + not_available: "Không sẵn sàng!" + categories_list: "Danh sách Danh mục" + filters: + with_topics: "%{filter} chủ đề" + with_category: "%{filter} %{category} chủ đề" + latest: + help: "chủ đề với bài viết gần nhất" + hot: + title: "Nổi bật" + read: + title: "Đọc" + search: + title: "Tìm kiếm" + help: "tìm trong tất cả chủ đề" + categories: + title: "Danh mục" + title_in: "Danh mục - {{categoryName}}" + new: + lower_title: "mới" + help: "chủ đề đã tạo cách đây vài ngày" + posted: + title: "Bài viết của tôi" + help: "chủ đề của bạn đã được đăng trong" + bookmarks: + title: "Đánh dấu" + help: "chủ để của bạn đã được đánh dấu" + category: + help: "Những chủ đề mới nhất trong chuyên mục{{categoryName}} " + top: + title: "Trên" + all: + title: "Từ trước tới nay" + yearly: + title: "Hàng năm" + quarterly: + title: "Hàng quý" + monthly: + title: "Hàng tháng" + weekly: + title: "Hàng tuần" + daily: + title: "Hàng ngày" + all_time: "Từ trước tới nay" + this_year: "Năm" + this_quarter: "Quý" + this_month: "Tháng" + this_week: "Tuần" + today: "Ngày" + other_periods: "xem top" + permission_types: + full: "Tạo / Trả lời / Xem" + create_post: "Trả lời / Xem" + readonly: "Xem" + admin_js: + type_to_filter: "gõ để lọc..." + admin: + title: 'Quản trị Diễn đàn' + moderator: 'Điều hành' + dashboard: + title: "Bảng điều khiển" + last_updated: "Bảng điều khiển cập nhật gần nhất:" + version: "Phiên bản" + up_to_date: "Bạn đã cập nhật phiên bản mới nhất" + critical_available: "Bản cập nhật quan trọng sẵn sằng." + updates_available: "Cập nhật đang sẵng sàng" + please_upgrade: "Vui lòng cập nhật!" + installed_version: "Đã cài đặt" + latest_version: "Mới nhất" + problems_found: "Tìm thấy vấn đề với bản cài đặt Discourse của bạn:" + last_checked: "Kiểm tra lần cuối" + refresh_problems: "Làm mới" + no_problems: "Không phát hiện vấn đề" + moderators: 'Điều hành:' + admins: 'Quản trị:' + blocked: 'Đã khóa:' + suspended: 'Đã tạm khóa:' + private_messages_short: "Tin nhắn" + private_messages_title: "Tin nhắn" + mobile_title: "Điện thoại" + space_free: "{{size}} trống" + uploads: "tải lên" + backups: "sao lưu" + traffic_short: "Băng thông" + traffic: "Application web requests" + page_views: "API Requests" + page_views_short: "API Requests" + show_traffic_report: "Xem chi tiết Báo cáo Lưu lượng" + reports: + today: "Hôm nay" + yesterday: "Hôm qua" + last_7_days: "7 Ngày gần nhất" + last_30_days: "30 Ngày gần nhất" + all_time: "Từ trước tới nay" + 7_days_ago: "7 Ngày trước" + 30_days_ago: "30 Ngày trước" + all: "Tất cả" + view_table: "bảng" + view_chart: "biểu đồ bar" + refresh_report: "Làm mới báo cáo" + start_date: "Từ ngày" + end_date: "Đến ngày" + commits: + latest_changes: "Thay đổi cuối: vui lòng cập nhật thường xuyên!" + by: "bởi" + flags: + title: "Gắn cờ" + old: "Cũ" + active: "Kích hoạt" + agree: "Đồng ý" + agree_flag_modal_title: "Đồng ý và..." + agree_flag_hide_post: "Đồng ý (ẩn bài viết + gửi PM)" + agree_flag_hide_post_title: "Ẩn bài viết này và tự động gửi tin nhắn đến người dùng hối thúc họ sửa nó" + agree_flag_restore_post: "Đồng ý (khôi phục bài viết)" + agree_flag_restore_post_title: "Khôi phục bài viết này" + agree_flag: "Đống ý với cờ này" + agree_flag_title: "Đồng ý với cờ này và giữ bài viết không thay đổi" + defer_flag: "Hoãn" + defer_flag_title: "Xóa cờ này; nó yêu cầu không có hành động nào vào thời điểm này." + delete: "Xóa" + delete_post_defer_flag_title: "Xóa bài viết; nếu là bài viết đầu tiên, xóa chủ đề này" + delete_post_agree_flag: "Xóa bài viết và Đồng ý với cờ" + delete_post_agree_flag_title: "Xóa bài viết; nếu là bài viết đầu tiên, xóa chủ đề này" + delete_flag_modal_title: "Xóa và..." + delete_spammer: "Xóa người Spam" + delete_spammer_title: "Xóa người dùng này và tất cả bài viết à chủ để của người dùng này." + disagree_flag_unhide_post: "Không đồng ý (ẩn bài viết)" + disagree_flag: "Không đồng ý" + clear_topic_flags: "Hoàn tất" + more: "(thêm trả lời...)" + dispositions: + agreed: "đồng ý" + disagreed: "không đồng ý" + deferred: "hoãn" + flagged_by: "Gắn cờ bởi" + system: "Hệ thống" + error: "Có lỗi xảy ra" + reply_message: "Trả lời " + no_results: "Không được gắn cờ" + summary: + action_type_3: + other: "sai chủ đề x{{count}}" + groups: + primary: "Nhóm Chính" + no_primary: "(không có nhóm chính)" + title: "Nhóm" + edit: "Sửa nhóm" + refresh: "Làm mới" + new: "Mới" + selector_placeholder: "nhập tên tài khoản" + name_placeholder: "Tên nhóm, không khoản trắng, cùng luật với tên tài khoản" + group_members: "Nhóm thành viên" + delete: "Xóa" + delete_confirm: "Xóa nhóm này?" + name: "Tên" + add: "Thêm" + add_members: "Thêm thành viên" + custom: "Tùy biến" + automatic: "Tự động" + primary_group: "Tự động cài là nhóm chính" + api: + generate_master: "Tạo Master API Key" + none: "Không có API keys nào kích hoạt lúc này." + user: "Thành viên" + title: "API" + key: "API Key" + generate: "Khởi tạo" + regenerate: "Khởi tạo lại" + revoke: "Thu hồi" + confirm_regen: "Bạn muốn thay API Key hiện tại bằng cái mới?" + all_users: "Tất cả Thành viên" + note_html: "Giữ khóa nào bảo mật, tất cả tài khoản có thể dùng khóa này để tạo bài viết với bất kỳ tài khoản nào." + plugins: + title: "Plugin" + installed: "Đã cài Plugin" + name: "Tên" + none_installed: "Bạn chưa cài plugin nào." + version: "Phiên bản" + enabled: "Kích hoạt" + is_enabled: "Có" + not_enabled: "Không" + change_settings: "Đổi Cấu hình" + change_settings_short: "Cấu hình" + howto: "Plugin cài như thế nào?" + backups: + title: "Bản sao lưu" + menu: + backups: "Bản sao lưu" + logs: "Log" + none: "Chưa có bản sao lưu." + read_only: + enable: + title: "Kích hoạt chế độ chỉ xem" + label: "Kích hoạt chế độ chỉ xem" + confirm: "Bạn muốn kích hoạt chế chộ chỉ xem?" + disable: + title: "Hủy chế độ chỉ xem này" + label: "Hủy chế độ chỉ xem" + logs: + none: "Chưa có log..." + columns: + filename: "Tên tập tin" + size: "Kích thước" + upload: + label: "Tải lên" + title: "Tải lên bản sao lưu cho phiên bản này" + uploading: "Đang tải lên..." + success: "'{{filename}}' đã tải lên thành công." + error: "Có lõi trong quá trình tải lên '{{filename}}': {{message}}" + operations: + is_running: "Tác vụ đang chạy..." + failed: "{{operation}} Thấy bại. Vui lòng xem log." + cancel: + label: "Hủy" + title: "Hủy tác vụ hiện tại" + confirm: "Bạn muốn hủy tác vụ hiện tại?" + backup: + label: "Sao lưu" + title: "Tạo bản sao lưu" + confirm: "Bạn muốn bắt đầu một bản sao lưu mới?" + without_uploads: "Đúng (không bao gồm những tập tin)" + download: + label: "Tải xuống" + title: "Tải xuống bản sao lưu này" + destroy: + title: "Xóa bản sao lưu này" + confirm: "Bạn muốn hủy bản sao lưu này?" + restore: + is_disabled: "Khôi phục đã bị cấm sử dụng trong cấu hình trang." + label: "Khôi phục" + title: "Khôi phục lại sao lưu này" + confirm: "Bạn muốn khôi phục bản sao lưu này?" + rollback: + label: "Rollback" + export_csv: + failed: "Xuất lỗi. Vui lòng kiểm tra log." + rate_limit_error: "Bài viết có thể tải về 1 lần mỗi này, vui lòng thử lại vào ngày mai." + button_text: "Xuất" + button_title: + user: "Xuất danh sách người dùng đầy đủ với định dạng CSV." + staff_action: "Xuất đầy đủ log hành động của nhân viên với định dạng CSV." + export_json: + button_text: "Xuất" + invite: + button_text: "Gửi Lời Mời" + button_title: "Gửi Lời Mời" + customize: + title: "Tùy biến" + long_title: "Tùy biến trang" + css: "CSS" + header: "Header" + top: "Trên" + footer: "Footer" + embedded_css: "Nhúng CSS" + head_tag: + text: "" + title: "HTML sẻ thêm trước thẻ " + body_tag: + text: "" + title: "HTML sẽ thêm trước thẻ " + override_default: "Không bao gồm style sheet chuẩn" + enabled: "Cho phép?" + preview: "xem trước" + undo_preview: "xóa xem trước" + save: "Lưu" + new: "Mới" + import: "Nhập" + import_title: "Chọn một file hoặc paste chữ." + delete: "Xóa" + delete_confirm: "Xóa tùy biến này?" + about: "Chỉnh sửa CSS và HTML header trên trang. Thêm tùy biến để bắt đầu." + color: "Màu sắc" + opacity: "Độ mờ" + copy: "Sao chép" + css_html: + title: "CSS/HTML" + long_title: "Tùy biến CSS và HTML" + colors: + title: "Màu sắc" + long_title: "Bảng màu" + about: "Chỉnh " + new_name: "Bản màu mới" + copy_name_prefix: "Bản sao của" + delete_confirm: "Xóa bảng màu này?" + undo: "hoàn tác" + undo_title: "Hoàn tác thay đổi của bạn vơ" + revert: "phục hồi" + revert_title: "Thiết lập lại màu về mặc định của Discourse." + primary: + name: 'chính' + description: 'Hầu hết chữ, biểu tượng, và viền.' + secondary: + name: 'cấp hai' + description: 'Màu nền, và màu chữ của một vài nút.' + tertiary: + name: 'cấp ba' + description: 'Liên kết, một và nút, thông báo, và màu nhấn.' + header_background: + name: "nền header" + description: "Màu nền header của trang." + header_primary: + name: "header chính" + highlight: + name: 'highlight' + danger: + name: 'nguy hiểm' + success: + name: 'thành công' + love: + name: 'đáng yêu' + description: "Màu của nút like" + wiki: + name: 'wiki' + email: + title: "Email" + settings: "Cấu hình" + all: "Tất cả" + sending_test: "Đang gửi Email test..." + error: "LỖI - %{server_error}" + test_error: "Có vấn đề khi gửi email test. Vui lòng kiểm tra lại cấu hình email của bạn, chắc chắn host mail của bạn không bị khóa kết nối, và thử lại." + sent: "Đã gửi" + skipped: "Đã bỏ qua" + sent_at: "Đã gửi vào lúc" + time: "Thời gian" + user: "Thành viên" + email_type: "Loại Email" + to_address: "Đến Địa chỉ" + test_email_address: "địa chỉ email để test" + send_test: "Gửi Email test" + sent_test: "đã gửi!" + refresh: "Tải lại" + format: "Định dạng" + html: "html" + text: "text" + last_seen_user: "Người dùng cuối:" + reply_key: "Key phản hồi" + skipped_reason: "Bỏ qua Lý do" + logs: + none: "Không tìm thấy log." + filters: + title: "Lọc" + user_placeholder: "tên người dùng" + address_placeholder: "name@example.com" + reply_key_placeholder: "key phản hồi" + skipped_reason_placeholder: "lý do" + logs: + title: "Log" + action: "Hành động" + created_at: "Đã tạo" + ip_address: "IP" + topic_id: "ID Chủ đề" + post_id: "ID Bài viết" + category_id: "ID Danh mục" + delete: 'Xoá' + edit: 'Sửa' + save: 'Lưu' + screened_actions: + block: "khóa" + do_nothing: "không làm gì" + staff_actions: + clear_filters: "Hiện thị mọi thứ" + staff_user: "Tài khoản Nhân viên" + subject: "Chủ đề" + when: "Khi" + details: "Chi tiết" + previous_value: "Trước" + new_value: "Mới" + diff: "So sánh" + show: "Hiển thị" + modal_title: "Chi tiết" + actions: + delete_user: "xóa người dùng" + change_trust_level: "thay đổi cấp tin cậy" + change_username: "thay đổi username" + change_site_setting: "thay đổi cấu hình trang" + change_site_customization: "thay đổi tùy biến trang" + delete_site_customization: "xóa tùy biến trang" + check_email: "kiểm tra email" + delete_topic: "xóa chủ đề" + delete_post: "xóa bài viết" + change_category_settings: "thay đổi cấu hình danh mục" + delete_category: "xóa danh mục" + create_category: "tạo danh mục" + screened_emails: + email: "Địa chỉ Email" + actions: + allow: "Cho phép" + screened_urls: + url: "URL" + domain: "Tên miền" + screened_ips: + rolled_up_no_subnet: "Không có gì để cuộn lên." + actions: + block: "Khóa" + do_nothing: "Cho phép" + allow_admin: "Cho phép Quản trị" + form: + label: "Mới:" + ip_address: "Địa chỉ IP" + add: "Thêm" + filter: "Tìm kiếm" + roll_up: + text: "Cuộn lên" + logster: + title: "Log lỗi" + impersonate: + title: "Mạo danh" + not_found: "Không tìm thấy người dùng này." + users: + title: 'Tài khoản' + create: 'Thêm tài khoản Quản trị' + last_emailed: "Email trước đây" + not_found: "Xin lỗi, username không tồn tại trong hệ thống." + id_not_found: "Xin lỗi, id người dùng không tồn tại trong hệ thống." + active: "Kích hoạt" + show_emails: "Hiện địa chỉ Email" + nav: + new: "Mới" + active: "Kích hoạt" + pending: "Đang chờ xử lý" + staff: 'Nhân viên' + suspended: 'Đã tạm khóa' + blocked: 'Đã khóa' + approved: "Đã duyệt?" + approved_selected: + other: "duyệt tài khoản ({{count}})" + reject_selected: + other: "từ chối tài khoản ({{count}})" + titles: + active: 'Thành viên kích hoạt' + new: 'Thành viên mới' + pending: 'Hoãn Xem xét Tài khoản' + newuser: 'Tài khoản ở Cấp độ Tin tưởng 0 (Tài khoản mới)' + basic: 'Tài khoản ở Cấp độ Tin tưởng 1 (Tài khoản Cơ bản)' + staff: "Nhân viên" + admins: 'Tài khoản Quản trị' + moderators: 'Điều hành viên' + blocked: 'Tài khoản Khóa' + suspended: 'Tài khoản Tạm khóa' + reject_successful: + other: "Từ chối thành công %{count} tài khoản." + reject_failures: + other: "Từ chối thất bại %{count} tài khoản." + not_verified: "Chưa xác thực" + check_email: + text: "Hiển thị" + user: + suspend_duration_units: "(ngày)" + suspend_reason: "Lý do" + suspended_by: "Tạm khóa bởi" + delete_all_posts: "Xóa tất cả bài viết" + suspend: "Tạm khóa" + unsuspend: "Đã mở khóa" + suspended: "Đã tạm khóa?" + moderator: "Mod?" + admin: "Quản trị?" + blocked: "Đã khóa?" + show_admin_profile: "Quản trị" + edit_title: "Sửa Tiêu đề" + save_title: "Lưu Tiêu đề" + refresh_browsers_message: "Tin nhắn đã gửi cho tất cả người dùng!" + show_public_profile: "Hiển thị hồ sơ công khai" + impersonate: 'Mạo danh' + ip_lookup: "Tìm kiếm địa chỉ IP" + log_out: "Đăng suất" + logged_out: "Thành viên đã đăng xuất trên tất cả thiết bị" + unblock: 'Mở khóa' + block: 'Khóa' + reputation: Danh tiếng + permissions: Quyền + activity: Hoạt động + last_100_days: 'trong 100 ngày gần đây' + private_topics_count: Chủ đề riêng tư + posts_read_count: Đọc bài viết + post_count: Bài đăng đã được tạo + topics_entered: Chủ để đã xem + warnings_received_count: Đã nhận Cảnh báo + approve: 'Duyệt' + approved_by: "duyệt bởi" + approve_success: "Thành viên được duyệt và đã gửi email hướng đẫn kích hoạt." + approve_bulk_success: "Thành công! Tất cả thành viên đã chọn được duyệt và thông báo." + time_read: "Thời gian đọc" + anonymize: "Tài khoản Nặc danh" + anonymize_confirm: "Bạn CHĂC CHẮN muốn xóa tài khoản nặc danh này? Nó sẽ thay đổi tên đăng nhập và email, và xóa tất cả thông tin trong hồ sơ." + anonymize_yes: "Đồng ý, đây là tài khoản nặc danh." + anonymize_failed: "Có vấn đề với những tài khoản nặc danh." + delete: "Xóa thành viên" + delete_forbidden_because_staff: "Admin và mod không thể xóa." + delete_posts_forbidden_because_staff: "Không thể xóa tất cả bài viết của quản trị và điều hành viên." + delete_confirm: "Bạn CHẮC CHẮN muốn xóa thành viên này? Nó là vĩnh viễn!" + delete_and_block: "Xóa và khóa email này và địa chỉ IP" + delete_dont_block: "Chỉ xóa" + deleted: "Thành viên này đã bị xóa" + delete_failed: "Có lỗi trong quá trình xóa thành viên này. Chắc chắn rằng tất cả bài viết đã được xóa trước khi xóa thành viên." + send_activation_email: "Gửi email kích hoạt" + activation_email_sent: "Email kích hoạt đã được gửi." + send_activation_email_failed: "Có vấn đề khi gửi lại email kích hoạt. %{error}" + activate: "Kích hoạt tài khoản" + activate_failed: "Có vấn đề khi kích hoạt thành viên này." + deactivate_account: "Vô hiệu hóa Tài khoản" + deactivate_failed: "Có vấn đề khi bỏ kích hoạt thành viên này." + unblock_failed: 'Có vẫn đề khi gỡ khóa thành viên này.' + block_failed: 'Có vấn đề khi khóa thành viên này.' + suspended_explanation: "Tài khoản tạm khóa không thể đăng nhập." + block_explanation: "Tài khoản bị khóa không thể đăng bài hoặc tạo chủ đề." + trust_level_change_failed: "Có lỗi xảy ra khi thay đổi mức độ tin tưởng của tài khoản." + suspend_modal_title: "Tạm khóa Thành viên" + lock_trust_level: "Khóa Cấp độ Tin tưởng" + tl3_requirements: + title: "Yêu cầu Cấp độ tin tưởng 3" + value_heading: "Giá trị" + requirement_heading: "Yêu cầu" + visits: "Lượt xem" + days: "ngày" + topics_viewed: "Đã xem chủ đề" + topics_viewed_all_time: "Đã xem chủ đề (mọi lúc)" + posts_read: "Đọc bài viết" + posts_read_all_time: "Đọc bài viết (mọi lúc)" + flagged_posts: "Đã gắn cờ Bài viết" + sso: + title: "Single Sign On" + external_id: "ID Bên ngoài" + external_username: "Tên đăng nhập" + external_name: "Tên" + external_email: "Email" + external_avatar_url: "URL Ảnh đại diện" + user_fields: + untitled: "Không có tiêu đề" + name: "Tên Trường" + type: "Loại Trường" + description: "Trường mô tả" + save: "Lưu" + edit: "Sửa" + delete: "Xoá" + cancel: "Hủy" + delete_confirm: "Bạn muốn xóa trường thành viên?" + options: "Lựa chọn" + required: + title: "Bắt buộc lúc đăng ký?" + enabled: "bắt buộc" + disabled: "không bắt buộc" + editable: + title: "Có thể chỉnh sửa sau khi đăng ký?" + enabled: "có thể chỉnh sửa" + disabled: "không thể chỉnh sửa" + show_on_profile: + title: "Hiển thị trong hồ sơ công khai" + enabled: "hiển thị trong hồ sơ" + disabled: "không hiển thị trong hồ sơ" + field_types: + text: 'Nội dung chữ' + confirm: 'Xác nhận' + dropdown: "Xổ xuống" + site_text: + title: 'Nội Dung Chữ' + site_settings: + show_overriden: 'Chỉ hiện thị đã ghi đè' + title: 'Xác lập' + reset: 'trạng thái đầu' + none: 'không có gì' + no_results: "Không tìm thấy kết quả." + clear_filter: "Xóa" + add_url: "thêm URL" + add_host: "thêm host" + categories: + all_results: 'Tất cả' + required: 'Bắt buộc' + basic: 'Cài đặt cơ bản' + users: 'Thành viên' + posting: 'Đang đăng bài' + email: 'Email' + files: 'Tập tin' + trust: 'Độ tin tưởng' + security: 'Bảo mật' + onebox: "Onebox" + seo: 'SEO' + spam: 'Rác' + developer: 'Nhà phát triển' + uncategorized: 'Khác' + backups: "Sao lưu" + login: "Đăng nhập" + plugins: "Plugins" + user_preferences: "Tùy chỉnh Tài khoản" + badges: + new: Mới + name: Tên + display_name: Tên Hiển thị + description: Mô tả + badge_grouping: Nhóm + reason_help: (Liên kết đến bài viết hoặc chủ đề) + save: Lưu + delete: Xóa + reason: Lý do + icon: Biểu tượng + image: Hình ảnh + trigger_type: + none: "Cập nhật hàng ngày" + preview: + bad_count_warning: + header: "CẢNH BÁO!" + sample: "Ví dụ:" + grant: + with: %{username} + with_post: %{username} for post in %{link} + emoji: + name: "Tên" + image: "Hình ảnh" + embedding: + confirm_delete: "Bạn muốn xóa host này?" + host: "Cho phép Host" + edit: "sửa" + category: "Đăng vào Danh mục" + add_host: "Thêm Host" + feed_settings: "Cấu hình Feed" + crawling_settings: "Cấu hình Crawler" + embed_blacklist_selector: "CSS selector for elements that are removed from embeds" + feed_polling_enabled: "Nhập bài viết bằng RSS/ATOM" + permalink: + title: "Liên kết cố định" + url: "URL" + topic_id: "ID Chủ đề" + topic_title: "Chủ đề" + post_id: "ID Bài viết" + post_title: "Bài viết" + category_id: "ID Danh mục" + category_title: "Danh mục" + external_url: "URL Bên ngoài" + form: + label: "Mới:" + add: "Thêm" + filter: "Tìm kiếm (URL hoặc External URL)" + lightbox: + download: "tải" + search_help: + title: 'Tìm giúp đỡ' + keyboard_shortcuts_help: + title: 'Phím tắt' + jump_to: + title: 'Chuyển đến' + home: 'g, h Trang chủ' + latest: 'g, l Cuối cùng' + new: 'g, n Mới' + unread: 'g, u Chưa đọc' + categories: 'g, c Danh mục' + top: 'g, t Trên' + bookmarks: 'g, b Đánh dấu' + profile: 'g, p Hồ sơ' + messages: 'g, m Tin nhắn' + navigation: + title: 'Điều hướng' + jump: '# Đến bài viết #' + back: 'u Quay lại' + open: 'o or Enter Mở chủ để đã chọn' + next_prev: 'shift+j/shift+k Next/previous section' + application: + title: 'Ứng dụng' + create: 'c Tạo mới chủ đề' + notifications: 'n Mở thông báo' + user_profile_menu: 'p Mở trình đơn thành viên' + show_incoming_updated_topics: '. Show updated topics' + search: '/ Tìm kiếm' + dismiss_new_posts: 'x, r Dismiss New/Posts' + dismiss_topics: 'x, t Bỏ qua bài viết' + log_out: 'shift+z shift+z Đăng xuất' + actions: + title: 'Hành động' + pin_unpin_topic: 'shift+p Pin/Unpin bài viết' + share_topic: 'shift+s Chia sẻ bài viết' + share_post: 's Chia sẻ bài viết' + reply_as_new_topic: 't Trả lời như là một liên kết đến bài viết' + reply_topic: 'shift+r Trả lời bài viết' + reply_post: 'r Trả lời bài viết' + like: 'l Thích bài viết' + bookmark: 'b Đánh dấu bài viết' + edit: 'e Sửa bài viết' + delete: 'd Xóa bài viết' + mark_watching: 'm, w theo dõi chủ đề' + badges: + allow_title: "có thể sử dụng như là tiêu đề" + more_badges: + other: "+%{count} Thêm" + none: "" + badge_grouping: + getting_started: + name: Bắt đầu + community: + name: Cộng đồng + trust_level: + name: Độ tin cậy + other: + name: Khác + posting: + name: Đang đăng bài + badge: + editor: + name: Biên tập + description: Chỉnh sửa bàn viết lần đầu + basic_user: + name: Cơ bản + member: + name: Thành viên + regular: + name: Thường xuyên + leader: + name: Lãnh đạo + welcome: + name: Chào mừng + description: Đã nhận 1 lượt thích + autobiographer: + description: Filled user profile information + anniversary: + name: Ngày kỷ niệm + good_post: + name: Bài viết tốt + great_post: + name: Bài viết tuyệt vời + nice_topic: + name: Bài viết hay + good_topic: + name: Chủ đề tốt + nice_share: + description: Đã chia sẻ bài viết với 25 lượt người truy cập + good_share: + description: Đã chia sẻ bài viết với 300 lượt người truy cập + great_share: + description: Đã chia sẻ bài viết với 1000 lượt người truy cập + first_like: + name: Lượt thích đầu tiên + description: Đã thích một bài đăng + first_flag: + name: Đánh dấu đầu tiên + description: Đánh dấu bài viết + promoter: + description: Đã mời một thành viên + campaigner: + description: Mời 3 thành viên (Độ tin cậy 1) + champion: + description: Mời 5 thành viên (Độ tin cậy 2) + first_share: + name: Chia sẽ đầu tiên + description: Chia sẽ bài viết + first_link: + name: Liên kết đầu tiên + description: Thêm một liên kết từ chủ để khác + first_quote: + name: Trích dẫn đầu tiên + description: Trích dẫn thành viên + read_guidelines: + name: Xem hướng dẫn + reader: + name: Người xem + description: Đọc tất cả bài viết trong các chủ để có hơn 100 bài + popular_link: + name: Liên kết phổ biến + hot_link: + name: Liên kết hấp dẫn + famous_link: + name: Liên kết phổ biến diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml new file mode 100644 index 0000000000..6df3552c2b --- /dev/null +++ b/config/locales/server.vi.yml @@ -0,0 +1,1027 @@ +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + +vi: + dates: + short_date_no_year: "D MMM" + short_date: "D MMM, YYYY" + long_date: "MMMM D, YYYY h:mma" + title: "Discourse" + topics: "Chủ đề" + posts: "bài viết" + loading: "Đang tải" + powered_by_html: 'Được hỗ trợ bởi Discourse, xem tốt nhất khi JavaScript được kích hoạt' + log_in: "Đăng nhập" + purge_reason: "Tự động xóa tài khoản không sử dụng, không kích hoạt." + disable_remote_images_download_reason: "Không thể tải ảnh về máy chủ vì thiếu dung lượng." + anonymous: "Ẩn danh" + errors: + format: '%{attribute} %{message}' + messages: + too_long_validation: "cho phép tối đa %{max} ký tự; bạn đã nhập %{length}." + invalid_boolean: "Giá trị boolean không hợp lệ" + taken: "đã được lấy trước" + accepted: phải được chấp nhận + blank: không thể để rỗng + present: phải để rỗng + confirmation: "%{attribute} không khớp" + empty: không thể để trống + equal_to: phải bằng %{count} + even: phải là chắn + exclusion: được bảo lưu + greater_than: phải lớn hơn %{count} + greater_than_or_equal_to: phải lớn hơn hoặc bằng %{count} + has_already_been_used: "đã được sử dụng" + inclusion: không được bao gồm trong danh sách + invalid: là không hợp lệ + is_invalid: "không hợp lệ; cố gắng cụ thể hơn một chút" + less_than: phải nhỏ hơn %{count} + less_than_or_equal_to: phải nhỏ hơn hoặc bằng %{count} + not_a_number: không phải là số + not_an_integer: phải là một số nguyên + odd: phải là số lẻ + record_invalid: 'Xác nhận thất bại: %{errors}' + restrict_dependent_destroy: + one: "Không thể xóa bản ghi bởi vì một bản ghi %{record} phụ thuộc đang tồn tại" + many: "Không thể xóa bản ghi bởi vì %{record} phụ thuộc tồn tại" + too_long: + other: quá dài (tối đa %{count} ký tự) + too_short: + other: quá ngắn (tối thiểu %{count} ký tự) + wrong_length: + other: độ dài không hợp lệ (nên đặt %{count} ký tự) + other_than: "phải khác %{count}" + template: + body: 'Đã có vấn đề với những trường sau:' + header: + other: '%{count} lỗi đã ngăn cản không thể lưu %{model} này' + embed: + load_from_remote: "Đã xảy ra lỗi khi tải bài viết." + site_settings: + min_username_length_exists: "Bạn không thể thiết lập chiều dài tối thiểu của username nhỏ hơn chiều dài của username ngắn nhất" + min_username_length_range: "Bạn không thiết lập giá trị nhỏ nhất lớn hơn giá trị lớn nhất" + max_username_length_exists: "Bạn không thể thiết lập chiều dài tối đa của username nhỏ hơn username dài nhất" + max_username_length_range: "Bạn không thể thiết lập số tối đa nhỏ hơn số tối thiểu" + default_categories_already_selected: "Bạn không thể chọn một danh mục được sử dụng trong danh sách khác." + s3_upload_bucket_is_required: "Bạn không thể tải lên S3 mà chưa thiết lập 's3_upload_bucket'." + bulk_invite: + file_should_be_csv: "Tập tin tải lên nên ở dạng csv hoặc txt." + backup: + operation_already_running: "Một tiến trình đang được thực hiện. Không thể bắt đầu một tiến trình mới ngay bây giờ." + backup_file_should_be_tar_gz: "Tập tin sao lưu nên nên ở dạng .tar.gz." + not_enough_space_on_disk: "Không đủ không gian trên đĩa để tải lên bản sao lưu này." + not_logged_in: "Bạn cần phải đăng nhập để thực hiện việc đó." + not_found: "Không thể tìm thấy đường dẫn hoặc tài nguyên yêu cầu." + invalid_access: "Bạn không được phép xem tài nguyên đã yêu cầu." + read_only_mode_enabled: "Trang web đang ở chế độ chỉ đọc. Tất cả các tương tác đã bị tắt." + too_many_replies: + other: "Xin lỗi bạn, người dùng mới tạm thời bị giới hạn với %{count} câu trả lời trong một chủ đề." + embed: + start_discussion: "Bắt đầu cuộc thảo luận" + continue: "Tiếp tục cuộc thảo luận" + more_replies: + other: "còn %{count} câu trả lời" + loading: "Đang tải cuộc thảo luận" + permalink: "Liên kết cố định" + imported_from: "Đây là cuộc thảo luận đi kèm chủ đề gốc tại %{link}" + in_reply_to: "▶ %{username}" + replies: + other: "%{count} câu trả lời" + no_mentions_allowed: "Xin lỗi, bạn không thể nhắc tới thành viên khác." + spamming_host: "Xin lỗi bạn không thể chèn liên kết tới trang đó." + user_is_suspended: "Người dùng đang bị treo không được phép đăng bài." + topic_not_found: "Có gì đó đã sai. Có lẽ chủ đề này đã bị đóng hoặc bị xóa trong khi bạn đang xem?" + just_posted_that: "rất giống với những gì bạn đã viết gần đây" + has_already_been_used: "đã được sử dụng" + invalid_characters: "chứa các kí tự không hợp lệ" + is_invalid: "không hợp lệ; cố gắng cụ thể hơn một chút" + next_page: "trang sau →" + prev_page: "← trang trước" + page_num: "Trang %{num}" + home_title: "Trang chủ" + topics_in_category: "Các chủ đề ở chuyên '%{category}'" + rss_posts_in_topic: "Nguồn cấp dữ liệu RSS của '%{topic}'" + rss_topics_in_category: "Nguồn cấp dữ liệu RSS của các chủ đề trong chuyên mục '%{category}'" + author_wrote: "%{author} đã viết:" + num_posts: "Bài đã đăng:" + num_participants: "Người tham gia:" + read_full_topic: "Đọc toàn bộ chủ đề" + private_message_abbrev: "Tin nhắn" + rss_description: + latest: "Chủ đề mới nhất" + hot: "Chủ đề nóng nhất" + posts: "Bài viết mới nhất" + too_late_to_edit: "Bài đăng đã được tạo từ rất lâu. Nó không thể được chỉnh sửa hoặc xóa nữa." + excerpt_image: "hình ảnh" + queue: + delete_reason: "Đã xóa thông qua hàng đợi kiểm duyệt" + groups: + errors: + can_not_modify_automatic: "Bạn không thể sửa đổi một nhóm tự động" + member_already_exist: "'%{username}' đã là thành viên của nhóm" + default_names: + everyone: "Mọi người" + admins: "quản trị" + moderators: "điều hành" + staff: "nhân viên" + trust_level_0: "trust_level_0" + trust_level_1: "trust_level_1" + trust_level_2: "trust_level_2" + trust_level_3: "trust_level_3" + trust_level_4: "trust_level_4" + education: + until_posts: + other: "%{count} bài đăng" + new-topic: | + Chào mừng bạn đến với %{site_name} — **cảm ơn vì đã đăng cuộc thảo luận mới!** + + - Bạn có cảm thấy tiêu đề có thú vị không khi bạn đọc to nó? Đó có phải là một đoạn tóm tắt tốt không? + + - Những ai sẽ hứng thú với cuộc thảo luận này? Tại sao đó lại trở thành vấn đề? Bạn muốn những loại phản hồi thế nào? + + - Sử dụng những từ khóa phổ biến để người khác có thể tìm thấy chủ đề của bạn dễ hơn. Để nhóm chủ đề của bạn với các chủ đề liên quan khác, hãy chọn chuyên mục cho chủ đề của mình. + + Xem thêm, [hướng dẫn cộng đồng của chúng tôi](/guidelines). Bảng điều khiển này sẽ chỉ xuất hiện vào lần đầu %{education_posts_text}. + new-reply: | + Chào mừng bạn đến với %{site_name} — **cám ơn vì đã đóng góp!** + + - Câu trả lời của bạn có làm cuộc thảo luận tốt hơn về mặt nào đó? + + - Hãy đối xử tốt với các thành viên khác trong cộng đồng của bạn. + + - Lời phê bình mang tính đóng góp cũng được chào đón, nhưng bạn nên phê bình *ý tưởng* chứ không phải con người. + + [Đọc hướng dẫn cộng đồng](/guidelines) để có thêm thông tin. Bảng này chỉ xuất hiện cho bài viết đầu tiên của bạn %{education_posts_text}. + avatar: | + ### How about a picture for your account? + + You've posted a few topics and replies, but your profile picture isn't as unique as you are -- it's just a letter. + + Have you considered **[visiting your user profile](%{profile_path})** and uploading a picture that represents you? + + It's easier to follow discussions and find interesting people in conversations when everyone has a unique profile picture! + sequential_replies: | + ### Xem xét việc trả lời nhiều bài viết cùng lúc + + Thay vì trả lời nhiều tuần tự đến từng chủ đề, xin vui lòng xem xét một bài trả lời duy nhất mà bao gồm các trích dẫn từ bài viết trước hoặc dùng tham chiếu @name. + + Bạn có thể sửa bài trả lời trước đó của bạn để thêm một trích dẫn bằng cách bôi đen và nhấn chọn nút quote reply vừa xuất hiện. + + Sẽ dễ dàng hơn cho tất cả mọi người để đọc chủ đề mà có ít câu trả lời sâu với nhiều cấp, trả lời cá nhân + dominating_topic: "### Hãy để người khác tham gia vào cuộc thảo luận\n\nChủ đề này rõ ràng là quan trọng với bạn & ndash; bạn đã đăng nhiều hơn% %{percent}% của các câu trả lời tại đây.\n\nBạn có chắc chắn bạn đang cung cấp đủ thời gian cho những người khác để chia sẻ quan điểm của mình? \n" + too_many_replies: | + ### Bạn đã đạt đến giới hạn trả lời cho chủ đề này + + Chúng tôi xin lỗi, nhưng người dùng mới bị giới hạn %{newuser_max_replies_per_topic} trả lời trong cùng một chủ đề. + + Thay vì thêm một câu trả lời khác, xin vui lòng xem xét chỉnh sửa trả lời trước đó của bạn, hoặc truy cập vào các chủ đề khác. + reviving_old_topic: "### Xem lại chủ đề này? \n\nCâu trả lời cuối cùng cho chủ đề này đã hơn hơn %{days} ngày. Trả lời của bạn sẽ đẩy chủ đề đó lên đầu danh sách và thông báo cho bất cứ ai liên quan đến cuộc thảo luận.\n\nBạn có chắc chắn bạn muốn tiếp tục cuộc trò chuyện cũ này? \n" + activerecord: + attributes: + category: + name: "Tên chuyên mục" + post: + raw: "Thân" + user_profile: + bio_raw: "GIới thiệu bản thân" + errors: + models: + topic: + attributes: + base: + warning_requires_pm: "Bạn chỉ có thể đính kèm cảnh báo qua tin nhắn cá nhân" + too_many_users: "Bạn chỉ có thể gửi một cảnh báo tới một người dùng mỗi lần." + cant_send_pm: "Xin lỗi, bạn không thể gửi tin nhắn tới thành viên này" + no_user_selected: "Bạn phải chọn một thành viên phù hợp." + user: + attributes: + password: + common: "là một trong 10000 mật khẩu được sử dụng nhiều nhất. Vui lòng sử dụng một mật khẩu an toàn hơn." + same_as_username: "giống với tên đăng nhập của bạn. Vui lòng sử dụng mật khẩu bảo mật hơn." + same_as_email: "giống với email của bạn. Vui lòng sử dụng mật khẩu bảo mật hơn" + ip_address: + signup_not_allowed: "Đăng ký không cho phép tài khoản này" + color_scheme_color: + attributes: + hex: + invalid: "không phải là một màu không hợp lệ" + user_profile: + no_info_me: "
Mục nói về bản thân bạn trong hồ sơ của bạn hiện đang trống, bạn muốn điền vào nó? " + no_info_other: "
%{name} đã không nhập bất cứ điều gì nói về bản thân của họ " + vip_category_name: "Phòng khách" + vip_category_description: "Một chuyên mục chỉ dành cho thành viên có mức tin tưởng 3 hoặc cao hơn" + meta_category_name: "Phản hồi" + meta_category_description: "Thảo luận về site này, tổ chức của nó, làm sao nó hoạt động, và làm sao chúng tôi có thể cải tiến nó tốt hơn." + staff_category_name: "Nhân viên" + staff_category_description: "Chuyên mục riêng dành cho nhân viên. Các chủ đề chỉ hiển thị với quản trị viên và điều hành viên." + assets_topic_body: "Chủ đề này tồn tại vĩnh viễn, chỉ xem được bởi nhân viên, dùng để chứa ảnh và file dành cho việc thiết kế. Vui lòng đừng xóa.\n\n\nHướng dẫn:\n\n\n1. Trả lời chủ đề này.\n2. Tải lên tất cả ảnh bạn cần cho logo, favicon, và các thứ khác. (Dùng nút tải lên trong công cụ viết bài, hoặc kéo-và-thả hoặc dán ảnh vào) \n3. Gửi trả lời của bạn.\n4. Bấm chuột phải lên ảnh trong bài đăng mới để chép đường dẫn của các ảnh đã được tải lên, hoặc sửa bài viết của bạn để lấy đường dẫn của ảnh. Sao chép các đường dẫn này.\n5. Dán các đường dẫn này vào [thiết lập chung](/admin/site_settings/category/required).\n\n\nNếu bạn cần tải lên các loại file khác, sửa `authorized_extensions` trong [thiết lập file](/admin/site_settings/category/files)." + lounge_welcome: + title: "Chào mừng bạn đến với Phòng khách" + body: |2 + + Chúc mừng! :confetti_ball: + + Nếu bạn có thể xem chủ đề này, bạn đã được thăng lên bậc **thường xuyên** (bậc tin tưởng 3). + + Bạn có thể … + + * Sửa tiêu đề của bất kì chủ đề nào + * Sửa chuyên mục của bất kì chủ đề nào + * Tất cả liên kết ở trạng thái follow ([những liên kết nofollow](http://en.wikipedia.org/wiki/Nofollow) sẽ được loại bỏ) + * Truy cập vào phòng khách dành riêng cho thành viên với bậc tin tưởng 3 hoặc cao hơn + * Ẩn bài viết spam với 1 lần đánh dấu. + + Đây là danh sách [của các thành viên thường xuyên](/badges/3/regular). Hãy chào họ đi nào. + + Cảm ơn vì đã trở thành một phần không thể thiếu đối với cộng đồng. + + (Để biết thêm chi tiết về bậc tin tưởng, [xem chủ đề này][trust]. Hãy nhớ rằng bạn phải tiếp tục đạt được các yêu cầu để duy trì bậc tin tưởng của mình.) + + [trust]: https://meta.discourse.org/t/what-do-user-trust-levels-do/4924 + category: + topic_prefix: "Giới thiệu chuyên mục %{category}" + errors: + uncategorized_parent: "Mục \"Chưa được phân loại\" không thể có một chuyên mục chính" + self_parent: "Cha của chủ đề phụ không thể nào là chính nó" + depth: "Bạn không thể để một chuyên mục con trong một chuyên mục con khác." + email_in_already_exist: "Địa chỉ thư đến '%{email_in}' đã được sử dụng cho danh mục '%{category_name}'" + cannot_delete: + uncategorized: "Không thể xoá mục Chưa phân loại" + has_subcategories: "Không thể xoá chuyên mục này được vì nó có chuyên mục con." + topic_exists: + other: "Không thể xoá phân loại này được bởi vì nó có %{count} chủ đề. Các chủ đề cũ là %{topic_link}." + topic_exists_no_oldest: "Không thể xoá chuyên mục này vì nó có %{count} chủ để." + trust_levels: + newuser: + title: "thành viên mới" + basic: + title: "thành viên cơ bản" + change_failed_explanation: "Bạn đã cố gắng để giảm hạng %{user_name} xuống '%{new_trust_level}'. Tuy nhiên cấp độ tin cậy hiện tại của họ đã là '%{current_trust_level}'. %{user_name} sẽ được giữ lại ở cấp độ '%{current_trust_level}' - nếu bạn muốn giảm hạng thành viên, trước tiên hãy khóa cấp độ tin cậy" + rate_limiter: + too_many_requests: "Hành động bạn vừa thực hiện bị giới hạn theo ngày. Hãy chờ %{time_left} và thử lại." + hours: + other: "%{count} giờ" + minutes: + other: "%{count} phút" + seconds: + other: "%{count} giây" + datetime: + distance_in_words: + half_a_minute: "< 1 phút" + less_than_x_seconds: + other: "< %{count} giây" + x_seconds: + other: "%{count} giây" + less_than_x_minutes: + other: "< %{count} phút" + x_minutes: + other: "%{count} phút" + about_x_hours: + other: "%{count} giờ" + x_days: + other: "%{count} ngày" + about_x_months: + other: "%{count} tháng" + x_months: + other: "%{count} tháng" + about_x_years: + other: "%{count} năm" + over_x_years: + other: "> %{count} năm" + almost_x_years: + other: "%{count} năm" + distance_in_words_verbose: + half_a_minute: "ngay bây giờ" + less_than_x_seconds: + other: "ngay bây giờ" + x_seconds: + other: "%{count} giây trước" + less_than_x_minutes: + other: "ít hơn %{count} phút trước" + x_minutes: + other: "%{count} phút trước" + about_x_hours: + other: "%{count} giờ trước" + x_days: + other: "%{count} ngày trước" + about_x_months: + other: "khoảng %{count} tháng trước" + x_months: + other: " %{count} tháng trước" + about_x_years: + other: "khoảng %{count} năm trước" + over_x_years: + other: "hơn %{count} năm trước" + almost_x_years: + other: "gần %{count} năm trước" + password_reset: + no_token: "Xin lỗi, liên kết đổi mật khẩu đã cũ. Chọn \"Đăng nhập\" và sử dụng chức năng \"Quên mật khẩu\" để lấy liên kết mới." + choose_new: "Vui lòng chọn mật khẩu mới" + choose: "Bạn phải nhập mật khẩu" + update: 'Cập nhật mật khẩu' + save: 'Nhập mật khẩu' + title: 'Thiết lập lại mật khẩu' + success: "Bạn đã thay đổi mật khẩu thành công và đã được đăng nhập." + success_unapproved: "Bạn đã thay đổi mật khẩu thành công." + continue: "Tiếp tục đến %{site_name}" + change_email: + confirmed: "Email của bạn đã được cập nhật." + please_continue: "Tiếp tục đến %{site_name}" + error: "Có một lỗi khi thay đổi địa chỉ email của bạn. Có lẽ email này đã được sử dụng rồi?" + activation: + action: "Nhấn vào đây để kích hoạt tài khoản của bạn" + already_done: "Xin lỗi, liên kết để xác nhận tài khoản này không còn hợp lệ. Có thể tài khoản của bạn được kích hoạt?" + please_continue: "Tài khoản của bạn đã được xác nhận; bạn sẽ được chuyển đến trang chủ." + continue_button: "Tiếp tục tới %{site_name}" + welcome_to: "Chào mừng bạn đến với %{site_name}!" + approval_required: "Một điều hành viên phải duyệt tài khoản của bạn trước khi bạn có thể đăng nhập diễn đàn này. Bạn sẽ nhận được email khi tài khoản của bạn được duyệt!" + missing_session: "Chúng tôi không thể xác" + post_action_types: + off_topic: + title: 'Không-đúng-chủ-đề' + description: 'Bài này không liên quan đến các cuộc thảo luận hiện nay theo quy định của các tiêu đề và bài đầu tiên, và có lẽ nó nên được di chuyển đến những nơi khác.' + long_form: 'đánh dấu không-đúng-chủ-đề' + spam: + title: 'Spam' + description: 'Bài đăng này là một bài quảng cáo. Không bổ ích hoặc liên quan tới chủ đề hiện tại, chỉ nhằm mục đích quảng cáo.' + long_form: 'đánh dấu là spam' + email_title: '"%{title}" đã bị gắn cờ spam' + email_body: "%{link}\n\n%{message}" + inappropriate: + title: 'Không thích hợp' + description: 'Chủ để này chứa nội dung mà bình thường được xem là xúc phạm, lạm dụng, hoặc vi phạm nguyên tắc cộng đồng.' + long_form: 'đánh dấu cái này không thích hợp' + notify_user: + long_form: 'đã nhắn tin cho thành viên' + email_title: 'Bài đăng của bạn trong "%{title}"' + email_body: "%{link}\n\n%{message}" + notify_moderators: + title: "Một thứ khác" + email_body: "%{link}\n\n %{message}" + bookmark: + title: "Đánh dấu chỉ mục \x1C" + description: 'Đánh dấu chỉ mục bài viết này' + long_form: 'đã đánh dấu chỉ mục bài viết này' + like: + title: 'Thích' + description: 'Thích bài viết này' + long_form: 'đã thích cái này' + vote: + title: 'Bầu chọn' + description: 'Bầu cho bài viết này' + long_form: 'bầu cho bài viết này' + topic_flag_types: + spam: + title: 'Rác' + description: 'Chủ đề này mang bản chất quảng cáo, không có ích và không thích hợp với nơi này.' + long_form: 'đã đánh dấu bài này dạng bài viết rác' + inappropriate: + title: 'Không phù hợp' + description: 'Chủ để này chứa nội dung mà với lý lẽ thường nhật được xem là xúc phạm, lạm dụng, hoặc vi phạm chỉ dẫn chung của cộng đồng.' + long_form: 'đã đánh dấu bài này không phù hợp' + notify_moderators: + title: "Một cái khác" + long_form: 'đã đánh dấu cho điều hành viên xem xét' + email_title: 'Chủ đề "%{title}" cần được ban điều hành quan tâm' + email_body: "%{link}\n\n%{message}" + flagging: + you_must_edit: '

Bài viết của bạn đã được gắn cờ bở cộng đồng. Vui lòngxem tin nhắn của bạn.

' + user_must_edit: '

Bài viết này đã bị đánh dấu bởi cộng đồng và đang được ẩn tạm thời.

' + archetypes: + regular: + title: "Chủ đề thường" + banner: + title: "Banner chủ đề" + message: + make: "Chủ đề này trở thành một banner. Nó sẽ hiện ở đầu mọi trang tới khi nó được tắt bởi thành viên." + remove: "Chủ đề này không còn là một banner. Nó sẽ không hiện ở đầu mọi trang nữa." + unsubscribed: + title: 'Hủy bỏ đăng ký' + description: "Bạn đã ngừng đăng. Chúng tôi sẽ không liên lạc bạn nữa!" + oops: "Trong trường hợp bạn không có ý thực hiện thao tác này, bấm vào bên dưới." + error: "Lỗi hủy đăng kí" + preferences_link: "Bạn có thể hủy theo dõi bản tin tóm tắt tại trang thiết lập" + different_user_description: "Bạn đang đăng nhập như một người dùng khác, không phải là người dùng đã được gửi đến qua mail. Hãy thoát ra và thử lại." + not_found_description: "Xin lỗi, chúng tôi không thể ngừng đăng ký bạn. Có thể là do link trong email của bạn đã hết hạn." + resubscribe: + action: "Đăng ký lại" + title: "Đã đăng ký lại!" + description: "Bạn đã được đăng ký lại." + reports: + visits: + title: "Các thành viên truy cập" + xaxis: "Ngày" + yaxis: "Số lần truy cập" + signups: + title: "Thành viên mới" + xaxis: "Ngày" + yaxis: "Số lượng thành viên mới" + profile_views: + title: "Xem hồ sơ người dùng" + xaxis: "Ngày" + yaxis: "Số người đã xem hồ sơ người dùng" + topics: + title: "Các chủ đề" + xaxis: "Ngày" + yaxis: "Số lượng chủ đề mới" + posts: + title: "Bài viết" + xaxis: "Ngày" + yaxis: "Số lượng bài viết mới" + likes: + title: "Lượt thích" + xaxis: "Ngày" + yaxis: "Số lượt thích mới" + flags: + title: "Dấu cờ - Flags" + xaxis: "Ngày" + yaxis: "Số dấu cờ - flag" + bookmarks: + title: "Các đánh dấu" + xaxis: "Ngày" + yaxis: "Số đánh dấu mới" + starred: + title: "Bắt đầu" + xaxis: "Ngày" + yaxis: "Số chủ đề được tạo." + users_by_trust_level: + title: "Thành viên ở mõi bậc tin tưởng" + xaxis: "Bậc tin tưởng" + yaxis: "Số thành viên" + emails: + title: "Email đã gửi" + xaxis: "Ngày" + yaxis: "Số lượng emails" + user_to_user_private_messages: + title: "Người dùng tới người dùng" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + system_private_messages: + title: "Hệ thống" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + moderator_warning_private_messages: + title: "Cảnh báo của điều hành viên" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + notify_moderators_private_messages: + title: "Thông báo ban quản trị" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + notify_user_private_messages: + title: "Thông báo người dùng" + xaxis: "Ngày" + yaxis: "Số lượng tin nhắn" + top_referrers: + title: "Giới thiệu hàng đầu" + xaxis: "Người dùng" + num_clicks: "Clicks" + num_topics: "Chủ đề" + top_traffic_sources: + title: "Nguồn truy cập" + xaxis: "Tên miền" + num_clicks: "Clicks" + num_topics: "Chủ đề" + num_users: "Người dùng" + top_referred_topics: + title: "Top chủ đề giới thiệu" + xaxis: "Chủ đề" + num_clicks: "Clicks" + page_view_anon_reqs: + title: "Ẩn danh" + xaxis: "Ngày" + yaxis: "Truy cập API ẩn danh" + page_view_logged_in_reqs: + title: "Đã đăng nhập" + xaxis: "Ngày" + yaxis: "Đã đăng nhập truy cập API" + page_view_crawler_reqs: + title: "Thu thập thông tin web" + xaxis: "Ngày" + yaxis: "Truy cập API thu thập thông tin web" + page_view_total_reqs: + title: "Tổng số" + xaxis: "Ngày" + yaxis: "Tổng số truy cập API" + page_view_logged_in_mobile_reqs: + title: "Trong yêu cầu đăng nhập API" + xaxis: "Ngày" + yaxis: "Mobile yêu cầu Đăng nhập API" + page_view_anon_mobile_reqs: + title: "Anon API Requests" + xaxis: "Ngày" + yaxis: "Mobile Anon API Requests" + http_background_reqs: + title: "Hình nền" + xaxis: "Ngày" + yaxis: "Yêu cầu đã sử dụng cho cập nhật thời gian thực và thống kê" + http_2xx_reqs: + title: "Trạng thái 2xx (OK)" + xaxis: "Ngày" + yaxis: "Yêu cầu thành công (Trạng thái 2xx)" + http_3xx_reqs: + title: "HTTP 3xx (Chuyển hướng)" + xaxis: "Ngày" + yaxis: "Chuyển hướng yêu cầu (Trạng thái 3xx)" + http_4xx_reqs: + title: "HTTP 4xx (Trình khách lỗi)" + xaxis: "Ngày" + yaxis: "Trình khách lỗi (Trạng thái 4xx)" + http_5xx_reqs: + title: "HTTP 5xx (Máy chủ lỗi)" + xaxis: "Ngày" + yaxis: "Máy chủ lỗi (Trạng thái 5xx)" + http_total_reqs: + title: "Tổng số" + xaxis: "Ngày" + yaxis: "Tổng số yêu cầu" + time_to_first_response: + title: "Thời gian để phản hồi lần đầu" + xaxis: "Ngày" + yaxis: "Thời gian trung bình (giờ)" + topics_with_no_response: + title: "Chủ đề không có phản hồi" + xaxis: "Ngày" + yaxis: "Tổng số" + mobile_visits: + title: "Các thành viên truy cập" + xaxis: "Ngày" + yaxis: "Số lần truy cập" + dashboard: + rails_env_warning: "Máy chủ của bạn đang chạy trong chế độ %{env}." + ruby_version_warning: "Bạn đang dùng một phiên bản Ruby 2.0.0 được biết là có nhiều vấn đề. Hãy nâng cấp bản vá 247 hoặc mới hơn." + host_names_warning: "Cài đặt của bạn config/database.yml đang sử dụng hostname mặc định. Cập nhật lại để sử dụng hostname của bạn" + gc_warning: 'Máy chủ của bạn hiện tại sử dụng cơ chế dọn rác mặc định của ruby, điều này khiến cho hiệu năng của máy chủ không tốt lắm. Đọc chủ đề sau cho việc tối ưu hiệu năng Tối ưu Ruby and Rails cho Discourse.' + sidekiq_warning: ' Sidekiq đang không hoạt động. Rất nhiều tác vụ, như gửi email, là được thực thi không đồng bộ bởi sidekiq. Hãy chắc chắn rằng ít nhất một tiến trình sidekiq phải đang hoạt động. Đọc thêm về Sidekiq tại đây.' + memory_warning: 'Máy chủ của bạn có bộ nhớ ít hơn 1 GB. Khuyến cáo sử dụng bộ nhớ tối thiểu 1 GB .' + google_oauth2_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Google OAuth2 (enable_google_oauth2_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' + facebook_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Facebook (enable_facebook_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' + twitter_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Twitter (enable_twitter_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' + github_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với GitHub (enable_github_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' + s3_config_warning: 'Máy chủ được cấu hình để upload file lên s3, tuy nhiên ít nhất một trong các tùy chỉnh sau đây không được thiết lập: s3_access_key_id, s3_secret_access_key hoặc s3_upload_bucket. Truy cập Thiết lập Site và bổ sung các thiết lập đó. Xem bài viết "How to set up image uploads to S3?" để biết thêm chi tiết.' + s3_backup_config_warning: 'Máy chủ được cấu hình để upload các bản sao lưu dữ liệu lên s3, tuy nhiên ít nhất một trong các tùy chỉnh sau đây không được thiết lập: s3_access_key_id, s3_secret_access_key hoặc s3_backup_bucket. Truy cập Thiết lập Site và bổ sung các thiết lập đó. Xem bài viết "How to set up image uploads to S3?" để biết thêm chi tiết.' + image_magick_warning: 'Máy chủ đã cấu hình để tạo hình đại diện nhỏ từ những hình lới, nhưng ImageMagick chưa được cài đặt. Cài ImageMagick sử dụng trình quản lý package yêu thích của bạn hoặc tải về phiên bản mới nhất.' + failing_emails_warning: 'Có %{num_failed_jobs} email jobs thấ bại. Kiểm tra app.yml và chắc chắn rằng cấu hình máy chủ email đúng. Xem jobs thất bại ở Sidekiq.' + default_logo_warning: "Cập nhập logo của trang. Cập nhập logo_url, logo_small_url, và favicon_url trong Thiết lập trang." + contact_email_invalid: "Email liên lạc của trang không hợp lệ. Cập nhật trong Thiết lập trang/a>." + title_nag: "Nhập tên trang của bạn. Cập nhập tiêu đề trong Thiết lập trang." + consumer_email_warning: "Trang web của bạn được cài đặt sử dụng Gmail (hoặc một dịch vụ email khác) để gửi email. Gmail có giới hạn số lượng email bạn có thể gửi. Hãy xem xét sử dụng một dịch vụ email khác như mandrill.com để đảm bảo khả năng vận chuyển tất cả các email." + site_settings: + censored_words: "Từ sẽ tự động thay thế bằng ■■■■" + delete_old_hidden_posts: "Tự động ẩn bất kỳ bài viết ở ẩn hơn 30 ngày." + default_locale: "Ngôn ngữ mặc định của Discourse (Mã ISO 639-1)" + allow_user_locale: "Cho phép thành viên chọn ngôn ngữ của riêng trong thiết lập giao diện." + min_post_length: "Số kí tự tối thiểu trong bài đăng." + min_first_post_length: "Chiều dài tối thiểu cho bài viết đầu tiên (nội dung chủ đề) tính theo ký tự." + min_private_message_post_length: "Số kí tự tối thiểu trong tin nhắn." + max_post_length: "Số kí tự tối đa trong bài đăng." + min_topic_title_length: "Số kí tự tối thiểu trong tiêu đề chủ đề." + max_topic_title_length: "Số kí tự tối đa trong tiêu đề chủ đề." + min_private_message_title_length: "Chiều dài tối thiểu cho phép theo số kí tự của một thông điệp" + min_search_term_length: "Số kí tự tối thiểu trong từ khóa tìm kiếm." + uncategorized_description: "Mô tả của chuyên mục \"Không phân loại\". Để trống khi không muốn mô tả." + allow_duplicate_topic_titles: "Cho phép các chủ đề trùng tiêu đề." + unique_posts_mins: "Trong bao nhiêu phút người sử dụng có thể viết bài khác với nội dung giống nhau" + title: "Tên của trang này, sử dụng trong thẻ tiêu đề" + site_description: "Mô tả trang này trong một câu, nó sẽ được sử dụng trong thẻ meta description" + contact_email: "Địa chỉ email liên hệ của người chịu trách nhiệm trang này. Sử dụng cho những thông báo quan trọng giống như cờ không được quản lý, cũng giống form liện hệ /about cho những vấn đề cấp bách." + contact_url: "URL liên hệ trong trang này. Sử dụng trong form liên hệ /about cho những vấn đề cấp bách." + queue_jobs: "DEVELOPER ONLY! WARNING! By default, queue jobs in sidekiq. If disabled, your site will be broken." + crawl_images: "Lấy hình ảnh tử URL bên ngoài để thêm vào đúng chiều dài và chiều cao." + download_remote_images_to_local: "Tải ảnh về lưu trữ để tránh ảnh bị hư." + download_remote_images_threshold: "Dung lượng tối thiểu cần để tải ảnh từ xa về lưu trữ (tính bằng phần trăm)" + disabled_image_download_domains: "Tải ảnh từ xa sẽ không áp dụng với các tên miền sau. Phân cách bằng dấu |" + post_edit_time_limit: "Tác giả có thể sửa hoặc xóa bài viết của họ trong (n) phút sau khi đăng. 0 là mãi mãi." + edit_history_visible_to_public: "Cho phép mọi người nhìn thấy phiên bản trước khi chỉnh sửa bài viết. Khi không cho phép, chỉ nhân viên có thể xem." + delete_removed_posts_after: "Bài viết đã được xóa bởi tác giả sẽ được tự động xóa sau (n) giờ. Nếu cài là 0, bài viết sẽ được xóa ngay lập tức." + max_image_width: "Chiều rộng tối đa của ảnh thu nhỏ trong bài viết." + max_image_height: "Chiều cao tối đa của ảnh thu nhỏ trong bài viết." + category_featured_topics: "Số chủ đề hiện thị mỗi danh mục trong trang /categories. Sau khi thay đổi giá trị này, nó sẽ mất khoảng 15 phút để trang danh mục cập nhật." + show_subcategory_list: "Hiện danh sách chuyên mục con thay vì danh sách chủ đề khi truy cập vào chuyên mục." + fixed_category_positions: "Nếu được bật, bạn sẽ có thể sắp xếp chuyên mục theo một thứ tự cố định. Nếu không bật, chuyên mục sẽ được sắp xếp theo thứ tử hoạt động." + fixed_category_positions_on_create: "Nếu chọn, sắp xếp danh mục sẽ được thực hiện trong cửa sổ tạo chủ đề (yêu cầu fixed_category_positions)." + post_excerpt_maxlength: "Chiều dài tối đa của đoạn trích / tóm tắt chủ đề." + favicon_url: "Favicon cho trang của bạn, xem tại http://en.wikipedia.org/wiki/Favicon, để chạy được với CDN ảnh phải là png" + mobile_logo_url: "Cố định vị trí hình logo sử dụng tại phía trên bên trái trang mobile của bạn. Nên là hình vuông. Nếu để trống, sẽ sử dụng `logo_url`. Ví dụ: http://example.com/uploads/default/logo.png" + apple_touch_icon_url: "Biểu tượng sử dụng trong các thiết bị cảm ứng của Apple. Kích thước gợi ý 144px x 144px" + email_custom_headers: "Danh sách xác định email header tùy chỉnh" + email_subject: "Tùy biến định dạng chủ đề cho chuẩn email. Xem tại https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" + use_https: "URL đầy đủ đến trang (Discourse.base_url) là http hoặc https? KHÔNG BẬT NÓ CHO TỚI KHI HTTPS ĐÃ CÀI ĐẶT SẴN SẰNG VÀ ĐÃ CHẠY!" + summary_score_threshold: "Số điểm tối thiểu yêu cầu cho một bài viết bao gồm 'Tóm tắt chủ đề này'" + summary_posts_required: "Số bài viết tối thiểu trong một chủ đề trước khi 'Tóm tắt chủ đề này' được kích hoạt" + summary_likes_required: "Số lượt thích trong một chủ đề trước khi 'Tóm tắt chủ đề này' được kích hoạt" + summary_percent_filter: "Khi người dùng nhấn 'Tóm tắt chủ đề này', hiển thị phí trên % của bài viết" + summary_max_results: "Số bài viết tối đa trả ra bởi 'Tóm tắt chủ đề này'" + cooldown_minutes_after_hiding_posts: "Số phút một người dùng phải chờ trước khi họ có thể sửa một bài viết ẩn bởi gắn cờ cộng đồng" + max_topics_in_first_day: "Số chủ đề tối đa một thành viên được tạo trong ngày đầu tiên." + max_replies_in_first_day: "Số trả lời tối đa một thành viên được tạo trong ngày đầu tiên" + tl2_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 2 (thành viên) bằng cách nhân với số này" + tl3_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 3 (bình thường) bằng cách nhân với số này" + tl4_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 4 (dẫn đầu) bằng cách nhân với số này" + ga_tracking_code: "Mã theo dõi Google analytics (ga.js), ví dụu: UA-12345678-9; chi tiết http://google.com/analytics" + ga_domain_name: "Tên miền Google analytics (ga.js), ví dụ: mysite.com; chi tiết http://google.com/analytics" + ga_universal_tracking_code: "Mã theo dõi Google Universal Analytics (analytics.js) , Ví dụ: UA-12345678-9; chi tiết http://google.com/analytics" + ga_universal_domain_name: "Tên miền Google Universal Analytics (analytics.js), ví dụ: mysite.com; chi tiết http://google.com/analytics" + enable_escaped_fragments: "Trả lại tới Google's Ajax-Crawling API nếu không xác định được webcrawler. Xem chi tiết https://support.google.com/webmasters/answer/174992?hl=en" + enable_noscript_support: "Cho phép webcrawler search engine chuẩn hỗ trợ bằng thẻ noscript" + allow_moderators_to_create_categories: "Cho phép điều hành viên tạo danh mục mới" + email_token_valid_hours: "Token quyên mật khẩu / kích hoạt tài khoản có giá trị trong (n) giờ." + email_token_grace_period_hours: "Token quyên mật khẩu / kích hoạt tài khoản vẫn còn giá trị (n) giờ sau khi được gia hạn" + enable_badges: "Kích hoạt hệ thống huy hiệu" + allow_index_in_robots_txt: "Chỉ rõ trong robots.txt trang web này cho phép tạo chỉ mục bởi web search engines." + email_domains_blacklist: "Một danh sách đuôi email mà người dùng không được phép dùng để đăng ký tài khoản. Ví dụ: maillinator.com|trashmail.net. Lưu ý mỗi tên miền cách nhau bởi dấu \"|\"." + email_domains_whitelist: "Danh sách tên miền người dùng ĐƯỢC PHÉP đăng ký tài khoản. CẢNH BÁO: người dùng với tên miền email khác trong danh sách sẽ không được phép đăng ký!" + forgot_password_strict: "Không thông báo cho người dùng tài không tồn tại khi họ dùng chức năng quyên mật khẩu." + log_out_strict: "Khi đăng xuất, đăng xuất TẤT CẢ session cho tất cả thiế bị" + version_checks: "Ping Discourse Hub để cập nhật phiên bản và hiện thông báo phiên bản mới trong bảng điều khiển quản trị" + new_version_emails: "Gửi email đến địa chỉ contact_email khi có phiên bản Discourse mới." + port: "DEVELOPER ONLY! WARNING! Sử dụng HTTP port thay vì mặc định port 80. Để trống mặc định port 80." + force_hostname: "DEVELOPER ONLY! LƯU Ý! Chỉ rõ hostname trong URL. Để trống là mặc định." + invite_expiry_days: "Key mời người dùng có giới hạn bao lâu? tính theo ngày" + invite_only: "Đăng ký tự do đã khóa, tất cả người dùng phải được mời bởi những thành viên khác hoặc nhân viên." + login_required: "Yêu cầu chứng thực để đọc nội dung trên trang web, không cho phép người dùng nặc danh truy cập." + min_username_length: "Chiều dài username tối thiểu." + max_username_length: "Chiều dài username tối đa." + reserved_usernames: "Những username không được phép đăng ký." + min_password_length: "Chiều dài mật khẩu tối thiểu." + block_common_passwords: "Không cho phép mật khẩu trong danh sách 10.000 mật khẩu phổ biến." + enable_sso: "Cho phép dùng single sign on bằng trang ngoài (CẢNH BÁO: ĐỊA CHỈ EMAIL CỦA NGƯỜI DÙNG PHẢI ĐƯỢC CHỨNG THỰC BỞI TRANG NGOÀI!)" + sso_url: "URL của single sign on enpoint" + sso_secret: "Chuỗi bảo mật đã được sử dụng để chứng thực thông tin SSO, chắc chắn nó có ít nhất 10 ký tự." + sso_not_approved_url: "Chuyển những tài khoản SSO chưa duyệt tới URL này" + allow_new_registrations: "Cho phép đăng ký người dùng mới. Bỏ chọn để bất cứ ai cũng có thể tạo tài khoản mới." + enable_yahoo_logins: "Cho phé chứng thực qua Yahoo" + enable_google_oauth2_logins: "Cho phép chứng thực qua Google Oauth2. Nó là cách chứng thực mà Google hỗ trợ. Yêu cầu key và secret." + google_oauth2_client_id: "Client ID ứng dụng Google của bạn." + google_oauth2_client_secret: "Client secret ứng dụng Google của bạn." + enable_twitter_logins: "Cho phép chứng thực qua Twitter, yêu cầu twitter_consumer_key và twitter_consumser_secret" + twitter_consumer_key: "Consumer key cho chứng thực Twitter, đăng ký tại http://dev.twitter.com" + twitter_consumer_secret: "Consumer secret cho chứng thực Twitter, đăng ký tại http://dev.twitter.com" + enable_facebook_logins: "Cho phép chứng thực Facebook, yêu cầu facebook_app_id và facebook_app_secret" + facebook_app_id: "App id cho chứng thực Facebook, đăng ký tại https://developers.facebook.com/apps" + facebook_app_secret: "App secret cho chứng thực Facebook, đăng ký tại https://developers.facebook.com/apps" + enable_github_logins: "Cho phép chứng thực Github, yêu cầu gitbug_client_id và githup_client_secret" + github_client_id: "Client id cho chứng thực Github, đăng ký tại https://github.com/settings/applications" + github_client_secret: "Client secret cho chứng thực Github, đăng ký tại https://github.com/settings/applications" + allow_restore: "Cho phép phục hồi, nó có thể thay thế TẤT CẢ dữ liệu trang web! Bỏ chọn, trừ khi bạn có kế hoạch phục hồi một bản sao lưu" + maximum_backups: "Số bản sao lưu tối đa lưu trong đĩa cứng. Những bản sao lưu cũ sẽ được xóa tự động" + automatic_backups_enabled: "Chạy sao lưu tự động như cấu hình trong tần số sao lưu" + backup_frequency: "Tần số sao lưu trang web, trong ngày." + enable_s3_backups: "Tải bản sao lưu lên S3 khi hoàn tất. QUAN TRỌNG: yêu cầu chứng thực S3 đã được nhập trong cấu hình File." + active_user_rate_limit_secs: "Tần số cập nhật trường 'last_seen_at, tính theo giây" + rate_limit_create_topic: "Sau khi tạo một chủ đề, người dùng phải chờ (n) giây trước khi tạo một chủ đề khác." + rate_limit_create_post: "Sau khi đăn bài, người dùng phải chờ (n) giây trước khi đăng bài khác." + rate_limit_new_user_create_topic: "Sau khi tạo một chủ đề, người dùng mới phải chờ (n) giây trước khi tạo chủ đề khác." + rate_limit_new_user_create_post: "Sau khi đăng bài, người dùng mới phải chờ (n) giây trước khi đăng bài khác." + max_likes_per_day: "Số tối đa người dùng có thể like mỗi ngày." + max_flags_per_day: "Số tối đa mà người dùng có thể gắn cờ mỗi ngày." + max_bookmarks_per_day: "Số tối đa người dùng có thể đánh dấu mỗi ngày." + max_edits_per_day: "Số tối đa người dùng có thể chỉnh sửa mỗi ngày." + max_topics_per_day: "Số chủ đề tối đa người dùng có thể tạo mỗi ngày." + max_private_messages_per_day: "Số tin nhắn tối đa người dùng có thể tạo mỗi ngày." + max_invites_per_day: "Số tối đa người dùng có thể gửi lời mời mỗi ngày." + suggested_topics: "Số chủ đề gợi ý hiện ở cuối một chủ đề" + limit_suggested_to_category: "Chỉ hiện thị những chủ đề từ danh mục hiện tại trong chủ đề gợi ý." + s3_access_key_id: "Amazon S3 access key id này sẽ được sử dụng để tải lên ảnh." + s3_secret_access_key: "Amazon S3 secret access key này sẽ được sử dụng để tải lên ảnh." + s3_region: "Amazon S3 region name sẽ được sử dụng để tải lên ảnh." + avatar_sizes: "Danh sách những kích thước hình đại diện tự động khởi tạo." + external_system_avatars_enabled: "Sử dụng dịch vụ ảnh đại diện bên ngoài." + default_invitee_trust_level: "Bậc tin tưởng mặc định (0-4) cho thành viên được mời." + tl1_requires_topics_entered: "Số chủ đề một thành viên mới phải truy cập trước khi được lên bậc tin tưởng 1" + tl1_requires_read_posts: "Số chủ đề một thành viên mới phải đọc trước khi được lên bậc tin tưởng 1" + tl1_requires_time_spent_mins: "Số phút một thành viên mới phải đọc trước khi được lên bậc tin tưởng 1" + tl2_requires_topics_entered: "Số chủ đề một thành viên mới phải truy cập trước khi được lên bậc tin tưởng 2" + tl2_requires_read_posts: "Số chủ đề một thành viên mới phải đọc trước khi được lên bậc tin tưởng 2" + tl2_requires_time_spent_mins: "Số phút một thành viên mới phải đọc trước khi được lên bậc tin tưởng 2" + min_trust_to_create_topic: "Bậc tin tưởng tối thiểu để tạo một chủ đề mới." + newuser_max_links: "Bao nhiêu liên kết tài khoản mới có thể thêm vào bài viết." + newuser_max_images: "Bao nhiêu hình tài khoản mới có thể thêm vào bài viết." + newuser_max_attachments: "Bao nhiêu đính kèm tài khoản mới có thể thêm vào bài viết" + email_time_window_mins: "Chờ (n) phút trước khi gửi bất kỳ một email thông báo nào, để cung cấp cho người dùng cơ hội để chỉnh sửa và hoàn tất bài viết của họ." + title_max_word_length: "Chiều dài tối đa chữ cho phép, tính theo ký tự, trong một tiêu đề chủ đề." + min_title_similar_length: "Chiều dài tối thiểu của tiêu đề trước khi kiểm tra trùng chủ đề." + min_body_similar_length: "Chiều dài tối thiểu của nội dung bài viết trước khi kiểm trang chủ đề tương tự." + category_colors: "Danh sách mã màu hexa cho phép cho danh mục." + max_attachment_size_kb: "Kích thước file tải lên tối đa tính theo kB. đã cấu hình trong nginx (client_max_body_size) / apache hoặc proxy." + authorized_extensions: "Danh sách định dạng file cho phép tải lên (sử dụng '*' để cho phép tất cả loại tập tin)" + reply_by_email_enabled: "Cho phép trả lời chủ đề qua email." + pop3_polling_ssl: "Sử dụng SSL khi kết nối tới POP3 server. (Đề nghị sử dụng)" + email_in_min_trust: "Bậc tin tưởng tối thiểu cho phép một thành viên gửi chủ đề mới qua email." + username_change_period: "Số ngày thành viên có thể thay đổi tên đăng nhập sau khi đăng kí (0 để vô hiệu hóa chức năng thay đổi tên thành viên)" + email_editable: "Cho phép thành viên thay đổi địa chỉ email sau khi đăng kí" + logout_redirect: "Trang chuyển hướng sau khi đăng xuất . Ví dụ : (http://somesite.com/logout)" + allow_uploaded_avatars: "Cho phép người dùng tải lên hình hồ sơ." + allow_animated_thumbnails: "Tạo ảnh động thu nhỏ cho ảnh .gif" + digest_min_excerpt_length: "Số kí tự tối thiểu của tóm tắt bài viết trong bản tin tóm tắt gửi qua email" + max_daily_gravatar_crawls: "Giới hạn số lần Discourse sẽ kiểm tra Gravatar mới trong một ngày" + allow_profile_backgrounds: "Cho phép người dùng tải lên ảnh nền" + suppress_uncategorized_badge: "Không hiển thị huy hiệu cho các chủ đề chưa phân loại trong danh sách chủ đề" + invites_per_page: "Lời mời mặc định hiển thị trên trang thành viên" + short_progress_text_threshold: "Sau khi số bài đăng của một chủ đề vượt qua giới hạn này, thanh tiến trình sẽ chỉ hiện số thứ tự của bài đăng hiện tại. Nếu bạn thay đổi chiều rộng của thanh tiến trình, bạn có thể cần thay đổi giá trị này" + show_create_topics_notice: "Nếu trang có ít hơn 5 chủ đề công khai, hiển thị một thông báo yêu cầu quản trị tạo thêm các chủ đề mới" + prevent_anons_from_downloading_files: "Cấm khách truy cập tải các tập tin đính kèm. CẢNH BÁO: việc này sẽ chặn những hình ảnh không thuộc giao diện trang hoạt động" + default_email_mailing_list_mode: "Mặc định gửi email cho mỗi bài viết mới." + default_email_always: "Mặc định gửi email thông báo mỗi khi người dùng kích hoạt." + default_other_external_links_in_new_tab: "Mặc định mở các liên kết ngoài trong thẻ mới " + errors: + invalid_email: "Địa chỉ email sai" + invalid_username: "Không có thành viên với tên đăng nhập này" + invalid_integer_min_max: "Giá trị phãi nằm giữa %{min} và %{max}." + invalid_integer_min: "Giá trị phải bằng %{min} hoặc lớn hơn" + invalid_integer_max: "Giá trị không thể cao hơn %{max}." + invalid_integer: "Giá trị phải là một số nguyên" + regex_mismatch: "Giá trị không giống với định dạng" + invalid_string: "Giá trị không hợp lệ." + invalid_string_min_max: "Phải nằm giữa %{min} và %{max} ký tự." + invalid_string_min: "Phải ít nhất %{min} ký tự." + invalid_string_max: "Không nhiều hơn %{max} ký tự." + invalid_reply_by_email_address: "Giá trị phải chứa '%{reply_key}' và phải khác với email thông báo." + notification_types: + mentioned: "%{display_username} đề cập bạn ở %{link}" + liked: "%{display_username} thích bài viết %{link} của bạn" + replied: "%{display_username} trả lời bài viết %{link} của bạn" + quoted: "%{display_username} trích dẫn bài viết %{link} của bạn" + edited: "%{display_username} sửa đổi bài viết %{link} của bạn" + posted: "%{display_username} viết bài ở %{link}" + moved_post: "%{display_username} di chuyển bài viết của bạn tới %{link}" + private_message: "%{display_username} gửi bạn một tin nhắn: %{link}" + invited_to_private_message: "%{display_username} mời bạn xem tin nhắn: %{link}" + invited_to_topic: "%{display_username} mời bạn xem chủ đề: %{link}" + invitee_accepted: "%{display_username} chấp nhận lời mời của bạn" + linked: "%{display_username} kết nối với bạn ở %{link}" + granted_badge: "Bạn kiếm được %{link}" + search: + within_post: "#%{post_number} bởi %{username}" + types: + category: 'Thư mục' + topic: 'Kết quả' + user: 'Thành viên' + sso: + not_found: "Không thể tìm hoặc tạo tài khoản, vui lòng liên hệ quản trị trang" + account_not_approved: "Tài khoản đang chờ duyệt, bạn sẽ nhận được email thông báo khi được duyệt" + unknown_error: "Có lỗi khi cập nhật thông tin, liên hệ quản trị trang" + timeout_expired: "Tài khoản đăng nhập bị quá thời gian, vui lòng đăng nhập lại" + original_poster: "Người viết gốc" + most_posts: "Bài viết Phỏ biến" + redirected_to_top_reasons: + new_user: "Chào mừng đến với cộng dồng của chúng tôi! Ở đây có những chủ để phổ biến." + not_seen_in_a_month: "Chào mừng quay trở lại! Chúng tôi thấy bạn truy cập một khoảng thời gian. Ở đây có những bài viết phổ biến từ lúc bạn đ." + change_owner: + deleted_user: "xóa người dùng" + topic_statuses: + archived_enabled: "Chủ đề này được đưa vào lưu trữ. Nó sẽ không được sửa đổi nữa. " + archived_disabled: "Chủ đề này được đưa khỏi lưu trữ. Nó có thể được sửa đổi." + closed_enabled: "Chủ đề này được đóng lại. Các trả lời mới sẽ không được chấp nhận." + closed_disabled: "Chủ đề này được mở ra. Các trả lời mới sẽ được chấp nhận." + autoclosed_enabled_lastpost_hours: + other: "Chủ đề này đã được đóng tự động %{count} giờ sau phản hồi cuối cùng. Không còn cho phép phản hồi mới." + autoclosed_disabled: "Chủ đề này đã được mở. Bạn có thể bình luận" + autoclosed_disabled_lastpost: "Chủ đề này đã được mở. Bạn có thể bình luận" + visible_enabled: "Chủ để này đã được lưu. Nó sẽ hiển thị trong danh sách chủ đề." + login: + not_approved: "Tài khoản của bạn chưa được kiểm duyệt. Bạn sẽ nhận được email thông báo khi bạn được phép đăng nhập." + incorrect_username_email_or_password: "Không đúng tài khoản, email hoặc mật khẩu" + wait_approval: "Cảm ơn bạn đã đăng ký. Chúng tôi sẽ thông báo sau khi tài khoản của bạn được kiểm duyệt." + active: "Tài khoản của bạn đã được kích hoạt và sẵn sàng để sử dụng." + not_activated: "Bạn không thể đăng nhập bây giờ. Chúng tôi đã gửi bạn một email kích hoạt tài khoản. Vui lòng làm theo hướng dẫn trong email để kích hoạt tài khoản của bạn." + not_allowed_from_ip_address: "Bạn không thể đăng nhập như là %{username} từ địa chỉ IP này." + admin_not_allowed_from_ip_address: "Bạn không thể đăng nhập như quản trị từ IP này." + suspended: "Bạn không thể đăng nhập cho tới ngày %{date}." + suspended_with_reason: "Tài khoản bị tạm khóa cho tới %{date}: %{reason}" + errors: "%{errors}" + not_available: "Không có sẵn. Thử %{suggestion}?" + something_already_taken: "Có lỗi xảy ra, tên đăng nhập hoặc email đã được đăng ký. Thử sử dụng chức năng quên mật khẩu." + omniauth_error: "Xin lỗi, có lỗi khi xác thực tài khoản của bạn. Bạn không được duyệt chứng thực?" + omniauth_error_unknown: "Cố lỗi xảy ra khi bạn đăng nhập, vui lòng thử lại." + new_registrations_disabled: "Đăng ký tài khoản mới không được cho phép tại thời điểm này." + password_too_long: "Mật khẩu giới hạn không quá 200 ký tự." + email_too_long: "Email bạn cung cấp quá dài. Địa chỉ email phải không quá 254 ký tự, và tên miền phải không quá 253 ký tự." + reserved_username: "Tên đăng nhập không được cho phép." + missing_user_field: "Bạn không hoàn tất tất cả các trường người dùng" + close_window: "Chứng thực hoàn tất. Đóng của sổ này để tiếp tụ." + user: + username: + characters: "chỉ bao gồm số, ký tự và dấu gạch dưới" + unique: "phải độc nhất" + blank: "phải hiện hành" + must_begin_with_alphanumeric: "phải bắt đầu bằng ký tự hoặc số hoặc gạch dưới" + must_end_with_alphanumeric: "phải kết thúc bằng ký tự hoặc số hoặc gạch dưới" + must_not_contain_confusing_suffix: "không chứ từ gây hiểu lầm như .json hoặc .png v.v..." + email: + not_allowed: "không được chấp nhận từ nhà cung cấp email đó. Vui long sử dụng địa chỉ email khác." + blocked: "không được chấp nhận." + ip_address: + blocked: "Đăng ký mới không cho phép từ địa chỉ IP của bạn." + invite_forum_mailer: + subject_template: "%{invitee_name} đã mời bạn gia nhập %{site_domain_name}" + text_body_template: | + %{invitee_name} đã mời bạn gia nhập + + > **%{site_title}** + > + > %{site_description} + + Nếu bạn không thích, nhấn vào link dưới đây: + + %{invite_link} + + Nó được mời từ một người dùng tin cập, bạn không cần đăng nhập. + invite_password_instructions: + subject_template: "Đặt mật khẩu cho tài khoản của bạn ở %{site_name}" + new_version_mailer: + subject_template: "[%{site_name}] Phiên bạn Discourse mới, cập nhật đã sẵn sàng" + new_version_mailer_with_notes: + subject_template: "[%{site_name}] cập nhật đã sẵn sàng" + flags_reminder: + please_review: "Vui lòng xem lại chúng." + post_number: "bài đăng" + flags_dispositions: + agreed: "Cảm ơn đã cho chúng tôi biết. Chúng thôi đồng ý nó là một vấn đề và chúng tôi sẽ xem xét nó." + agreed_and_deleted: "Cảm ơn đã cho chúng tôi biết thông tin. Chúng tôi đồng ý đây là một vấn đề và chúng tôi đã xóa bài viết này." + disagreed: "Cảm ơn đã cho chúng tôi biết thông tin. Chúng tôi đang xem xét nó." + deferred: "Cảm ơn đã cho chúng tôi biết thông tin. Chúng tôi đang xem xét nó." + system_messages: + welcome_user: + subject_template: "Chào mừng đến với %{site_name}!" + welcome_invite: + subject_template: "Chào mừng đến với %{site_name}!" + backup_succeeded: + subject_template: "Sản sao lưu hoàn tất thành công" + backup_failed: + subject_template: "Sao lưu lỗi." + text_body_template: | + Sao lưu lỗi. + + Đây là log: + + ``` + %{logs} + ``` + restore_succeeded: + subject_template: "Phục hồi thành công" + text_body_template: "Phục hồi đã thành công." + restore_failed: + subject_template: "Phục hồi thất bại." + text_body_template: | + Phục hồi thất bại. + + Đây là log: + + ``` + %{logs} + ``` + csv_export_succeeded: + subject_template: "Xuất dữ liệu hoàn tất" + csv_export_failed: + subject_template: "Xuất dữ liệu thất bại" + text_body_template: "Chúng tôi xin lỗi, những dữ liệu bạn xuất bị lỗi. Vui lòng xem log hoặc liên hệ nhân viên." + email_reject_no_account: + subject_template: "[%{site_name}] Vấn đề Email -- Không xác định tài khoản" + email_reject_empty: + subject_template: "[%{site_name}] Vấn đề Email -- Không có nội dung" + email_reject_parsing: + subject_template: "[%{site_name}] Vấn đề Email-- Không nhận dạng được nội dung" + email_reject_invalid_access: + subject_template: "[%{site_name}] Vấn đề Email -- truy cập không phù hợp" + email_reject_post_error: + subject_template: "[%{site_name}] Vấn đề Email -- Lỗi đăng bài" + email_reject_post_error_specified: + subject_template: "[%{site_name}] Vấn đề Email -- Lỗi đăng bài" + email_reject_reply_key: + subject_template: "[%{site_name}] Vấn đề Email -- Không xác định được key trả lời" + email_reject_destination: + subject_template: "[%{site_name}] Vấn đề Email -- Không xác định địa chỉ Đến:" + email_reject_topic_not_found: + subject_template: "[%{site_name}] Vấn đề Email -- Không tìm thấy chủ đề" + email_reject_topic_closed: + subject_template: "[%{site_name}] Vấn đề Email -- Chủ đề đóng" + email_reject_auto_generated: + subject_template: "[%{site_name}] Vấn đề Email -- Tự động tạo trả lời" + email_error_notification: + subject_template: "[%{site_name}] Vấn đề Email -- chứng thực POP lỗi" + too_many_spam_flags: + subject_template: "Tài khoản mới bị chặn" + blocked_by_staff: + subject_template: "Tài khoản bị khóa" + unblocked: + subject_template: "Tài khoản được mở khóa" + pending_users_reminder: + subject_template: + other: "%{count} thành viên đang chờ duyệt" + subject_re: "Re:" + subject_pm: "[PM]" + user_notifications: + previous_discussion: "Các trả lời trước" + unsubscribe: + title: "Bỏ theo dõi" + description: "Bạn không thích nhận mail giống mail này? Nhấn vào bỏ theo dõi để bỏ đăng ký ngay lập tức:" + posted_by: "Đăng bởi %{{username} ngày %{post_date}" + user_invited_to_private_message_pm: + subject_template: "[%{site_name}] %{username} mời bạn trả lời thông điệp '%{topic_title}'" + user_invited_to_topic: + subject_template: "[%{site_name}] %{username} mời bạn trả lời chủ đề '%{topic_title}'" + user_replied: + subject_template: "[%{site_name}] %{topic_title}" + user_quoted: + subject_template: "[%{site_name}] %{topic_title}" + user_mentioned: + subject_template: "[%{site_name}] %{topic_title}" + user_posted: + subject_template: "[%{site_name}] %{topic_title}" + user_posted_pm: + subject_template: "[%{site_name}] [PM] %{topic_title}" + digest: + why: "Tóm tắt %{site_link} từ lần cuối truy cập %{last_seen_at}" + subject_template: "[%{site_name}] Tóm tắt" + new_activity: "Hoạt động mới ở chủ đề và bài viết của bạn:" + top_topics: "Bài viết phổ biến" + other_new_topics: "Chủ đề phổ biến" + click_here: "bấm vào đây" + from: "%{site_name} tóm tắt" + read_more: "Đọc Tiếp" + more_topics: "Đây là %{new_topics_since_seen} những chủ đề mới khác." + more_topics_category: "Thêm chủ đề mới:" + forgot_password: + subject_template: "[%{site_name}] Đặt lại mật khẩu" + set_password: + subject_template: "[%{site_name}] Đặt Mật khẩu" + admin_login: + subject_template: "[%{site_name}] Đăng nhập" + account_created: + subject_template: "[%{site_name}] Tài khoản mới" + authorize_email: + subject_template: "[%{site_name}] Xác nhận địa chỉ email mới của bạn" + signup_after_approval: + subject_template: "Bạn đã được kiểm duyệt ở %{site_name}!" + signup: + subject_template: "[%{site_name}] Xác nhận tài khoản mới của bạn" + page_not_found: + title: "Trang bạn yêu cầu không tồn tại hoặc riêng tư." + popular_topics: "Phổ biến" + recent_topics: "Gân đây" + see_more: "Thêm" + search_title: "Tìm trang này" + search_google: "Goole" + login_required: + welcome_message: | + #[Chào mừng đến %{title}](#welcome) + Trang này yêu cầu phải có tài khoản. Vui lòng tạo một tài khoản hoặc đăng nhập để tiếp tục. + terms_of_service: + title: "Điều khoản Dịch vụ" + signup_form_message: 'Tôi đã đọc và đồng ý với Điều khoản dịch vụ.' + deleted: 'đã bị xóa ' + upload: + edit_reason: "tải về một bản sao của hình ảnh." + unauthorized: "Xin lỗi, tập tin của bạn tải lên không được cho phép (định dạng cho phép: %{authorized_extensions})." + pasted_image_filename: "Hình ảnh được chèn" + store_failure: "Tải lên lỗi #%{upload_id} cho tài khoản #%{user_id}." + file_missing: "Xin lỗi, bạn phải cung cấp tập tin để tải lên" + attachments: + too_large: "Xin lỗi, tập tin bạn tải lên quá lớn (kích thước tối đa %{max_size_kb}KB)." + images: + too_large: "Xin lỗi, hình bạn tải lên quá lớn (kích thước tối đa %{max_size_kb}KB), Vui lòng chỉnh lại kích thước và thử lại." + size_not_found: "Xin lỗi, không thể xác định kích thước hình. Có thể hình của bạn bị lỗi?" + avatar: + missing: "Xin lỗi, hình đại diện bạn chọn không có sãn trên máy chủ. Bạn có thể tải lên lại?" + email_log: + no_user: "không tìm thấy người dùng với id %{user_id}" + anonymous_user: "Người dùng là nặc danh" + suspended_not_pm: "Tài khoản bị tạm khóa, không có tin nhắn" + seen_recently: "Tài khoản đã xem gần đây" + post_not_found: "Không tìm thấy bài viết với id %{post_id}" + notification_already_read: "Thông báo email này đã được đọc" + topic_nil: "post.topic is nil" + post_deleted: "bài viết đã bị xóa bởi tác giả" + user_suspended: "người dùng đã bị tạm khóa" + already_read: "người dùng đã đọc bài viết này" + message_blank: "tin nhắn rỗng" + message_to_blank: "message.to rỗng" + text_part_body_blank: "text_part.body rỗng" + body_blank: "nội dung rỗng" + color_schemes: + base_theme_name: "Cơ bản" + about: "Giới thiệu" + guidelines: "Hướng dẫn" + privacy: "Riêng tư" + edit_this_page: "Sửa trang này" + csv_export: + boolean_yes: "Đồng ý" + boolean_no: "Không" + guidelines_topic: + title: "FAQ/Hướng dẫn" + tos_topic: + title: "Điều khoản Dịch vụ" + privacy_topic: + title: "Chính sách Riêng tư" + admin_login: + success: "Gửi mail lỗi" + error: "Lỗi!" + email_input: "Email quản trị" + submit_button: "Gửi email" + performance_report: + initial_topic_title: Báo cáo hiệu suất website diff --git a/plugins/poll/config/locales/client.vi.yml b/plugins/poll/config/locales/client.vi.yml new file mode 100644 index 0000000000..f183ac832c --- /dev/null +++ b/plugins/poll/config/locales/client.vi.yml @@ -0,0 +1,37 @@ +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + +vi: + js: + poll: + voters: + other: "người bình chọn" + total_votes: + other: "tổng số bình chọn" + average_rating: "Đánh giá trung bình: %{average}." + multiple: + help: + between_min_and_max_options: "Bạn có thể chọn giữa %{min}%{max}." + cast-votes: + title: "Bỏ phiếu của bạn" + label: "Bình chọn ngay!" + show-results: + title: "Hiển thị kết quả cuộc thăm dò" + label: "Hiện kết quả" + hide-results: + title: "Trở lại bầu chọn của bạn" + label: "Ẩn kết quả" + open: + title: "Mở bình chọn" + label: "Mở" + confirm: "Bạn có chắc mở bình chọn này?" + close: + title: "Đóng bình chọn" + label: "Đóng lại" + confirm: "Bạn có chắc chắn muốn đóng bình chọn này?" + error_while_toggling_status: "Có lỗi trong khi chuyển đổi qua lại các trạng thái của bình chọn này." + error_while_casting_votes: "Có lỗi trong khi tạo mãu bầu chọn của bạn" diff --git a/plugins/poll/config/locales/server.vi.yml b/plugins/poll/config/locales/server.vi.yml new file mode 100644 index 0000000000..760397cb10 --- /dev/null +++ b/plugins/poll/config/locales/server.vi.yml @@ -0,0 +1,31 @@ +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + +vi: + site_settings: + poll_enabled: "Cho phép người dùng tạo các cuộc thăm dò?" + poll_maximum_options: "Số lượng tối đa tùy chọn trong một cuộc thăm dò." + poll: + multiple_polls_without_name: "Có nhiều cuộc thăm dò mà không có tên. Sử dụng thuộc tính 'name' để xác định cuộc thăm dò của bạn." + multiple_polls_with_same_name: "Có nhiều cuộc thăm dò có cùng tên: %{name}. Sử dụng thuộc tính 'name' để xác định cuộc thăm dò của bạn." + default_poll_must_have_at_least_2_options: "Thăm dò ý kiến ​​phải có ít nhất 2 lựa chọn." + named_poll_must_have_at_least_2_options: "Thăm dò có tên %{name} phải có ít nhất 2 lựa chọn." + default_poll_must_have_different_options: "Thăm dò ý kiến ​​phải có các tùy chọn khác nhau." + named_poll_must_have_different_options: "Thăm dò %{name} ​​phải có các tùy chọn khác nhau." + default_poll_with_multiple_choices_has_invalid_parameters: "Thăm dò ý kiến ​​với nhiều sự lựa chọn có các tham số không hợp lệ." + named_poll_with_multiple_choices_has_invalid_parameters: "Thăm dò %{name} ​​với nhiều sự lựa chọn có các tham số không hợp lệ. " + requires_at_least_1_valid_option: "Bạn phải chọn ít nhất 1 lựa chọn hợp lệ." + cannot_change_polls_after_5_minutes: "Bạn không thể thêm, xóa hoặc đổi tên các cuộc thăm dò 5 phút đầu tiên." + op_cannot_edit_options_after_5_minutes: "Bạn không thể thêm hoặc loại bỏ các bình chọn sau khi 5 phút đầu tiên. Hãy liên hệ với người điều hành nếu bạn cần chỉnh sửa một bình chọn nào đó." + staff_cannot_add_or_remove_options_after_5_minutes: "Bạn không thể thêm hoặc loại bỏ các bình chọn sau khi 5 phút đầu tiên. Bạn nên đóng chủ đề này và tạo ra một cái mới để thay thế." + no_polls_associated_with_this_post: "Không có cuộc thăm dò được liên kết với bài này." + no_poll_with_this_name: "Không có thăm dò có tên %{name} liên kết với bài viết này." + post_is_deleted: "Không thể thực hiện trên bài viết đã xóa." + topic_must_be_open_to_vote: "Các chủ đề phải được mở để bầu chọn." + poll_must_be_open_to_vote: "Thăm dò ý kiến ​​phải được mở để bầu chọn." + topic_must_be_open_to_toggle_status: "Các chủ đề phải được mở để chuyển trạng thái." + only_staff_or_op_can_toggle_status: "Chỉ có một BQT hoặc các người đăng bài có thể chuyển đổi một trạng thái thăm dò ý kiến" diff --git a/public/403.vi.html b/public/403.vi.html new file mode 100644 index 0000000000..0af9f470dc --- /dev/null +++ b/public/403.vi.html @@ -0,0 +1,26 @@ + + + Bạn không thể làm được điều đó (403) + + + + +
+

403

+

Bạn không thể xem tài nguyên đó!

+ +

Trang này sẽ được thay thế bằng một trang lỗi 403 tùy chỉnh của Discourse.

+
+ + \ No newline at end of file diff --git a/public/422.vi.html b/public/422.vi.html new file mode 100644 index 0000000000..6c782e6655 --- /dev/null +++ b/public/422.vi.html @@ -0,0 +1,25 @@ + + + Thay đổi bạn muốn thực hiện đã bị từ chối (422) + + + + + +
+

Thay đổi bạn muốn thực hiện đã bị từ chối.

+

Có thể bạn đã cố thay đổi một số tính năng mà bạn không thể truy cập.

+
+ + \ No newline at end of file diff --git a/public/500.vi.html b/public/500.vi.html new file mode 100644 index 0000000000..6330c4373b --- /dev/null +++ b/public/500.vi.html @@ -0,0 +1,12 @@ + + + Ôi - Lỗi 500 + + + +

Ối

+

Phần mềm chạy diễn đàn thảo luận này bất ngờ gặp sự cố không mong muốn. Chúng tôi rất xin lỗi vì sự bất tiện này.

+

Thông tin chi tiết về lỗi đã được ghi lại và thông báo tự động đã được tạo. Chúng tôi sẽ xem xét lỗi này.

+

Không cần tiến hành bất cứ hành động nào. Tuy nhiên, nếu lỗi này vẫn tiếp tục, bạn có thể cung cấp thêm thông tin chi tiết bao gồm các bước để tái tạo lỗi hoặc tạo một thảo luận trên meta category.

+ + diff --git a/public/503.vi.html b/public/503.vi.html new file mode 100644 index 0000000000..c6d73355d5 --- /dev/null +++ b/public/503.vi.html @@ -0,0 +1,11 @@ + + + Trang đang được bảo trì- Discourse.org + + + +

Trang tạm dừng dịch vụ để bảo trì dịch vụ

+

Vui lòng quay lại sau vài phút.

+

Xin lỗi về sự bất tiện này!

+ + \ No newline at end of file diff --git a/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml new file mode 100644 index 0000000000..a9c1c2782f --- /dev/null +++ b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml @@ -0,0 +1,12 @@ +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + +vi: + site_settings: + enable_imgur: "Kích hoạt imgur api để tải file lên, không lưu trữ file tại máy chủ." + imgur_client_id: "Client ID imgur.com của bạn, cần cho chức năng tải ảnh lên. " + imgur_client_secret: "Client secret của bạn tạo imgur.com. Hiện tại không cần để tải ảnh lên, nhưng sẽ có trong tương lai." \ No newline at end of file From 003399bf96b19c48cf6bdc9ee603ed9f0cafea36 Mon Sep 17 00:00:00 2001 From: "Khoa, Le Ngoc" Date: Fri, 22 Jan 2016 14:54:48 +0700 Subject: [PATCH 004/140] Update translation code --- config/locales/{client.vi_VN.yml => client.vi.yml} | 9 ++++++++- config/locales/{server.vi_VN.yml => server.vi.yml} | 9 ++++++++- .../config/locales/{client.vi_VN.yml => client.vi.yml} | 2 +- .../config/locales/{server.vi_VN.yml => server.vi.yml} | 2 +- public/{403.vi_VN.html => 403.vi.html} | 0 public/{422.vi_VN.html => 422.vi.html} | 0 public/{500.vi_VN.html => 500.vi.html} | 0 public/{503.vi_VN.html => 503.vi.html} | 0 .../locale/{server.vi_VN.yml => server.vi.yml} | 2 +- 9 files changed, 19 insertions(+), 5 deletions(-) rename config/locales/{client.vi_VN.yml => client.vi.yml} (99%) rename config/locales/{server.vi_VN.yml => server.vi.yml} (99%) rename plugins/poll/config/locales/{client.vi_VN.yml => client.vi.yml} (99%) rename plugins/poll/config/locales/{server.vi_VN.yml => server.vi.yml} (99%) rename public/{403.vi_VN.html => 403.vi.html} (100%) rename public/{422.vi_VN.html => 422.vi.html} (100%) rename public/{500.vi_VN.html => 500.vi.html} (100%) rename public/{503.vi_VN.html => 503.vi.html} (100%) rename vendor/gems/discourse_imgur/lib/discourse_imgur/locale/{server.vi_VN.yml => server.vi.yml} (98%) diff --git a/config/locales/client.vi_VN.yml b/config/locales/client.vi.yml similarity index 99% rename from config/locales/client.vi_VN.yml rename to config/locales/client.vi.yml index 0ff10f7685..a3da018129 100644 --- a/config/locales/client.vi_VN.yml +++ b/config/locales/client.vi.yml @@ -1,4 +1,11 @@ -vi_VN: +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + +vi: js: number: format: diff --git a/config/locales/server.vi_VN.yml b/config/locales/server.vi.yml similarity index 99% rename from config/locales/server.vi_VN.yml rename to config/locales/server.vi.yml index cfea124c7c..e02c1a55d9 100644 --- a/config/locales/server.vi_VN.yml +++ b/config/locales/server.vi.yml @@ -1,4 +1,11 @@ -vi_VN: +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + +vi: dates: short_date_no_year: "D MMM" short_date: "D MMM, YYYY" diff --git a/plugins/poll/config/locales/client.vi_VN.yml b/plugins/poll/config/locales/client.vi.yml similarity index 99% rename from plugins/poll/config/locales/client.vi_VN.yml rename to plugins/poll/config/locales/client.vi.yml index 1bbdb53fdf..e0fec57092 100644 --- a/plugins/poll/config/locales/client.vi_VN.yml +++ b/plugins/poll/config/locales/client.vi.yml @@ -1,4 +1,4 @@ -vi_VN: +vi: js: poll: voters: diff --git a/plugins/poll/config/locales/server.vi_VN.yml b/plugins/poll/config/locales/server.vi.yml similarity index 99% rename from plugins/poll/config/locales/server.vi_VN.yml rename to plugins/poll/config/locales/server.vi.yml index 52ccdf117c..e5c0d30eec 100644 --- a/plugins/poll/config/locales/server.vi_VN.yml +++ b/plugins/poll/config/locales/server.vi.yml @@ -1,4 +1,4 @@ -vi_VN: +vi: site_settings: poll_enabled: "Cho phép người dùng tạo các cuộc thăm dò?" poll_maximum_options: "Số lượng tối đa tùy chọn trong một cuộc thăm dò." diff --git a/public/403.vi_VN.html b/public/403.vi.html similarity index 100% rename from public/403.vi_VN.html rename to public/403.vi.html diff --git a/public/422.vi_VN.html b/public/422.vi.html similarity index 100% rename from public/422.vi_VN.html rename to public/422.vi.html diff --git a/public/500.vi_VN.html b/public/500.vi.html similarity index 100% rename from public/500.vi_VN.html rename to public/500.vi.html diff --git a/public/503.vi_VN.html b/public/503.vi.html similarity index 100% rename from public/503.vi_VN.html rename to public/503.vi.html diff --git a/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi_VN.yml b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml similarity index 98% rename from vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi_VN.yml rename to vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml index 7bbf9c106b..6b70e6db43 100644 --- a/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi_VN.yml +++ b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml @@ -1,4 +1,4 @@ -vi_VN: +vi: site_settings: enable_imgur: "Kích hoạt imgur api để tải file lên, không lưu trữ file tại máy chủ." imgur_client_id: "Client ID imgur.com của bạn, cần cho chức năng tải ảnh lên. " From 4bce8f37b0dbee5e59299c204c6c0691f78142dd Mon Sep 17 00:00:00 2001 From: "Khoa, Le Ngoc" Date: Fri, 22 Jan 2016 16:07:30 +0700 Subject: [PATCH 005/140] Add language mapping "vi_VN: vi" --- .tx/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tx/config b/.tx/config index bba3633be7..1377a12a53 100644 --- a/.tx/config +++ b/.tx/config @@ -1,6 +1,6 @@ [main] host = https://www.transifex.com -lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt +lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, vi_VN: vi [discourse-org.clientenyml] file_filter = config/locales/client..yml From 9d96c6d43595657ff348cca87036c985c039db39 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 29 Jan 2016 21:35:16 +0100 Subject: [PATCH 006/140] Prepare settings file for Docker based phpBB3 importer --- script/import_scripts/phpbb3/settings.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/import_scripts/phpbb3/settings.yml b/script/import_scripts/phpbb3/settings.yml index d7ee6174e2..279900feef 100644 --- a/script/import_scripts/phpbb3/settings.yml +++ b/script/import_scripts/phpbb3/settings.yml @@ -16,8 +16,9 @@ import: # This is the path to the root directory of your current phpBB installation (or a copy of it). # The importer expects to find the /files and /images directories within the base directory. + # You need to change this to something like /var/www/phpbb if you are not using the Docker based importer. # This is only needed if you want to import avatars, attachments or custom smilies. - phpbb_base_dir: /var/www/phpbb + phpbb_base_dir: /shared/import/data site_prefix: # this is needed for rewriting internal links in posts From 15165440e5cd7e8379a2a8198394d6e6385684ba Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 4 Feb 2016 20:39:42 +0100 Subject: [PATCH 007/140] Map :-) to :slightly_smiling: during phpBB3 import --- script/import_scripts/phpbb3/support/smiley_processor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/import_scripts/phpbb3/support/smiley_processor.rb b/script/import_scripts/phpbb3/support/smiley_processor.rb index 1737e446ce..0b0785fd3f 100644 --- a/script/import_scripts/phpbb3/support/smiley_processor.rb +++ b/script/import_scripts/phpbb3/support/smiley_processor.rb @@ -28,7 +28,7 @@ module ImportScripts::PhpBB3 def add_default_smilies { [':D', ':-D', ':grin:'] => ':smiley:', - [':)', ':-)', ':smile:'] => ':smile:', + [':)', ':-)', ':smile:'] => ':slightly_smiling:', [';)', ';-)', ':wink:'] => ':wink:', [':(', ':-(', ':sad:'] => ':frowning:', [':o', ':-o', ':eek:'] => ':astonished:', From 0ef141b2c34d6b0e86ebb8df81a457dca2973bb4 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 5 Feb 2016 08:48:16 +1100 Subject: [PATCH 008/140] FIX: skip jwt encoding for auth --- lib/auth/google_oauth2_authenticator.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index 0f40c91f0c..c51e5a01d0 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -31,12 +31,15 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator end def register_middleware(omniauth) + # jwt encoding is causing auth to fail in quite a few conditions + # skipping omniauth.provider :google_oauth2, :setup => lambda { |env| strategy = env["omniauth.strategy"] strategy.options[:client_id] = SiteSetting.google_oauth2_client_id strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret - } + }, + skip_jwt: true end protected From 46589a1a0c1a56361f9bf0268a1d25882ed74bbf Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 25 Jan 2016 14:27:59 +0800 Subject: [PATCH 009/140] FEATURE: AR adapter to failover to a replica DB server. --- app/models/global_setting.rb | 5 +- config/database.yml | 8 +- config/discourse_defaults.conf | 6 + .../postgresql_fallback_adapter.rb | 136 ++++++++++++++++++ .../postgresql_fallback_adapter_spec.rb | 55 +++++++ 5 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 lib/active_record/connection_adapters/postgresql_fallback_adapter.rb create mode 100644 spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index 10866f8e8b..b1bf927f2f 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -18,11 +18,14 @@ class GlobalSetting def self.database_config hash = {"adapter" => "postgresql"} - %w{pool timeout socket host port username password}.each do |s| + %w{pool timeout socket host port username password replica_host replica_port}.each do |s| if val = self.send("db_#{s}") hash[s] = val end end + + hash["adapter"] = "postgresql_fallback" if hash["replica_host"] + hostnames = [ hostname ] hostnames << backup_hostname if backup_hostname.present? diff --git a/config/database.yml b/config/database.yml index fb6d923aea..19da362a0d 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,7 +1,13 @@ development: prepared_statements: false - adapter: postgresql + adapter: postgresql_fallback + host: 172.17.0.2 + port: 6432 database: discourse_development + username: tgxworld + password: test + replica_host: 172.17.0.3 + replica_port: 6432 min_messages: warning pool: 5 timeout: 5000 diff --git a/config/discourse_defaults.conf b/config/discourse_defaults.conf index 19c1ce63d1..0ff3b8af81 100644 --- a/config/discourse_defaults.conf +++ b/config/discourse_defaults.conf @@ -43,6 +43,12 @@ db_password = # see: https://github.com/rails/rails/issues/21992 db_prepared_statements = false +# host address for db replica server +db_replica_host = + +# port running replica db server, defaults to 5432 if not set +db_replica_port = + # hostname running the forum hostname = "www.example.com" diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb new file mode 100644 index 0000000000..18f76d6ec5 --- /dev/null +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -0,0 +1,136 @@ +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/postgresql_adapter' +require 'discourse' +require 'concurrent' + +class TaskObserver + def update(time, result, ex) + if result + logger.info { "PG connection heartbeat successfully returned #{result}" } + elsif ex.is_a?(Concurrent::TimeoutError) + logger.warning { "PG connection heartbeat timed out".freeze } + else + if ex.message.include?("PG::UnableToSend") + logger.info { "PG connection heartbeat: Master connection is not active.".freeze } + else + logger.error { "PG connection heartbeat failed with error: \"#{ex}\"" } + end + end + end + + private + + def logger + Rails.logger + end +end + +module ActiveRecord + module ConnectionHandling + def postgresql_fallback_connection(config) + master_connection = postgresql_connection(config) + + replica_connection = postgresql_connection(config.dup.merge({ + host: config[:replica_host], port: config[:replica_port] + })) + verify_replica(replica_connection) + + klass = ConnectionAdapters::PostgreSQLFallbackAdapter.proxy_pass(master_connection.class) + klass.new(master_connection, replica_connection, logger, config) + end + + private + + def verify_replica(connection) + value = connection.raw_connection.exec("SELECT pg_is_in_recovery()").values[0][0] + raise "Replica database server is not in recovery mode." if value == 'f' + end + end + + module ConnectionAdapters + class PostgreSQLFallbackAdapter < AbstractAdapter + ADAPTER_NAME = "PostgreSQLFallback".freeze + MAX_FAILURE = 5 + HEARTBEAT_INTERVAL = 5 + + attr_reader :main_connection + + def self.all_methods(klass) + methods = [] + + (klass.ancestors - AbstractAdapter.ancestors).each do |_klass| + %w(public protected private).map do |level| + methods << _klass.send("#{level}_instance_methods", false) + end + end + + methods.flatten.uniq.sort + end + + def self.proxy_pass(klass) + Class.new(self) do + (self.all_methods(klass) - self.all_methods(self)).each do |method| + self.class_eval <<-EOF + def #{method}(*args, &block) + proxy_method(:#{method}, *args, &block) + end + EOF + end + end + end + + def initialize(master_connection, replica_connection, logger, config) + super(nil, logger, config) + + @master_connection = master_connection + @main_connection = @master_connection + @replica_connection = replica_connection + @failure_count = 0 + load! + end + + def proxy_method(method, *args, &block) + @main_connection.send(method, *args, &block) + rescue ActiveRecord::StatementInvalid => e + if e.message.include?("PG::UnableToSend") && @main_connection == @master_connection + @failure_count += 1 + + if @failure_count == MAX_FAILURE + Discourse.enable_readonly_mode if !Discourse.readonly_mode? + @main_connection = @replica_connection + load! + connection_heartbeart(@master_connection) + @failure_count = 0 + else + proxy_method(method, *args, &block) + end + end + + raise e + end + + private + + def load! + @visitor = @main_connection.visitor + @connection = @main_connection.raw_connection + end + + def connection_heartbeart(connection, interval = HEARTBEAT_INTERVAL) + timer_task = Concurrent::TimerTask.new(execution_interval: interval) do |task| + connection.reconnect! + + if connection.active? + @main_connection = connection + load! + Discourse.disable_readonly_mode if Discourse.readonly_mode? + task.shutdown + end + end + + timer_task.add_observer(TaskObserver.new) + timer_task.execute + end + end + end +end diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb new file mode 100644 index 0000000000..e0195e7d82 --- /dev/null +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' +require_dependency 'active_record/connection_adapters/postgresql_fallback_adapter' + +describe ActiveRecord::ConnectionAdapters::PostgreSQLFallbackAdapter do + let(:master_connection) { ActiveRecord::Base.connection } + let(:replica_connection) { master_connection.dup } + let(:adapter) { described_class.new(master_connection, replica_connection, nil, nil) } + + before :each do + ActiveRecord::Base.clear_all_connections! + end + + describe "proxy_method" do + context "when master connection is not active" do + before do + replica_connection.stubs(:send) + master_connection.stubs(:send).raises(ActiveRecord::StatementInvalid.new('PG::UnableToSend')) + master_connection.stubs(:reconnect!) + master_connection.stubs(:active?).returns(false) + + @old_const = described_class::HEARTBEAT_INTERVAL + described_class.const_set("HEARTBEAT_INTERVAL", 0.1) + end + + after do + Discourse.disable_readonly_mode + described_class.const_set("HEARTBEAT_INTERVAL", @old_const) + end + + it "should set site to readonly mode and carry out failover and switch back procedures" do + expect(adapter.main_connection).to eq(master_connection) + adapter.proxy_method('some method') + expect(Discourse.readonly_mode?).to eq(true) + expect(adapter.main_connection).to eq(replica_connection) + + master_connection.stubs(:active?).returns(true) + sleep 0.15 + + expect(Discourse.readonly_mode?).to eq(false) + expect(adapter.main_connection).to eq(master_connection) + end + end + + it 'should raise errors not related to the database connection' do + master_connection.stubs(:send).raises(StandardError.new) + expect { adapter.proxy_method('some method') }.to raise_error(StandardError) + end + + it 'should proxy methods successfully' do + expect(adapter.proxy_method(:execute, 'SELECT 1').values[0][0]).to eq("1") + expect(adapter.proxy_method(:active?)).to eq(true) + expect(adapter.proxy_method(:raw_connection)).to eq(master_connection.raw_connection) + end + end +end From 0058d09e35bf765ce1e91bbd636549ecb24c7348 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 26 Jan 2016 15:46:51 +0800 Subject: [PATCH 010/140] Second attempt which removes any kind proxying. --- config/database.yml | 8 +- .../postgresql_fallback_adapter.rb | 114 ++++-------------- .../postgresql_fallback_adapter_spec.rb | 78 +++++++----- 3 files changed, 72 insertions(+), 128 deletions(-) diff --git a/config/database.yml b/config/database.yml index 19da362a0d..fb6d923aea 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,13 +1,7 @@ development: prepared_statements: false - adapter: postgresql_fallback - host: 172.17.0.2 - port: 6432 + adapter: postgresql database: discourse_development - username: tgxworld - password: test - replica_host: 172.17.0.3 - replica_port: 6432 min_messages: warning pool: 5 timeout: 5000 diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index 18f76d6ec5..df5a23b9e9 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -14,7 +14,7 @@ class TaskObserver logger.info { "PG connection heartbeat: Master connection is not active.".freeze } else logger.error { "PG connection heartbeat failed with error: \"#{ex}\"" } - end + end end end @@ -28,15 +28,21 @@ end module ActiveRecord module ConnectionHandling def postgresql_fallback_connection(config) - master_connection = postgresql_connection(config) + begin + connection = postgresql_connection(config) + rescue PG::ConnectionBad => e + connection = postgresql_connection(config.dup.merge({ + "host" => config["replica_host"], "port" => config["replica_port"] + })) - replica_connection = postgresql_connection(config.dup.merge({ - host: config[:replica_host], port: config[:replica_port] - })) - verify_replica(replica_connection) + verify_replica(connection) - klass = ConnectionAdapters::PostgreSQLFallbackAdapter.proxy_pass(master_connection.class) - klass.new(master_connection, replica_connection, logger, config) + Discourse.enable_readonly_mode if !Discourse.readonly_mode? + + start_connection_heartbeart(connection, config) + end + + connection end private @@ -45,92 +51,24 @@ module ActiveRecord value = connection.raw_connection.exec("SELECT pg_is_in_recovery()").values[0][0] raise "Replica database server is not in recovery mode." if value == 'f' end - end - module ConnectionAdapters - class PostgreSQLFallbackAdapter < AbstractAdapter - ADAPTER_NAME = "PostgreSQLFallback".freeze - MAX_FAILURE = 5 - HEARTBEAT_INTERVAL = 5 + def interval + 5 + end - attr_reader :main_connection + def start_connection_heartbeart(existing_connection, config) + timer_task = Concurrent::TimerTask.new(execution_interval: interval) do |task| + connection = postgresql_connection(config) - def self.all_methods(klass) - methods = [] - - (klass.ancestors - AbstractAdapter.ancestors).each do |_klass| - %w(public protected private).map do |level| - methods << _klass.send("#{level}_instance_methods", false) - end - end - - methods.flatten.uniq.sort - end - - def self.proxy_pass(klass) - Class.new(self) do - (self.all_methods(klass) - self.all_methods(self)).each do |method| - self.class_eval <<-EOF - def #{method}(*args, &block) - proxy_method(:#{method}, *args, &block) - end - EOF - end + if connection.active? + existing_connection.disconnect! + Discourse.disable_readonly_mode if Discourse.readonly_mode? + task.shutdown end end - def initialize(master_connection, replica_connection, logger, config) - super(nil, logger, config) - - @master_connection = master_connection - @main_connection = @master_connection - @replica_connection = replica_connection - @failure_count = 0 - load! - end - - def proxy_method(method, *args, &block) - @main_connection.send(method, *args, &block) - rescue ActiveRecord::StatementInvalid => e - if e.message.include?("PG::UnableToSend") && @main_connection == @master_connection - @failure_count += 1 - - if @failure_count == MAX_FAILURE - Discourse.enable_readonly_mode if !Discourse.readonly_mode? - @main_connection = @replica_connection - load! - connection_heartbeart(@master_connection) - @failure_count = 0 - else - proxy_method(method, *args, &block) - end - end - - raise e - end - - private - - def load! - @visitor = @main_connection.visitor - @connection = @main_connection.raw_connection - end - - def connection_heartbeart(connection, interval = HEARTBEAT_INTERVAL) - timer_task = Concurrent::TimerTask.new(execution_interval: interval) do |task| - connection.reconnect! - - if connection.active? - @main_connection = connection - load! - Discourse.disable_readonly_mode if Discourse.readonly_mode? - task.shutdown - end - end - - timer_task.add_observer(TaskObserver.new) - timer_task.execute - end + timer_task.add_observer(TaskObserver.new) + timer_task.execute end end end diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index e0195e7d82..66dd0707c0 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -1,55 +1,67 @@ require 'rails_helper' require_dependency 'active_record/connection_adapters/postgresql_fallback_adapter' -describe ActiveRecord::ConnectionAdapters::PostgreSQLFallbackAdapter do - let(:master_connection) { ActiveRecord::Base.connection } - let(:replica_connection) { master_connection.dup } - let(:adapter) { described_class.new(master_connection, replica_connection, nil, nil) } - - before :each do - ActiveRecord::Base.clear_all_connections! +describe ActiveRecord::ConnectionHandling do + let(:config) do + ActiveRecord::Base.configurations["test"].merge({ + "adapter" => "postgresql_fallback", + "replica_host" => "localhost", + "replica_port" => "6432" + }) end - describe "proxy_method" do - context "when master connection is not active" do + after do + ActiveRecord::Base.clear_all_connections! + Discourse.disable_readonly_mode + end + + describe "#postgresql_fallback_connection" do + it 'should return a PostgreSQL adapter' do + expect(ActiveRecord::Base.postgresql_fallback_connection(config)) + .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) + end + + context 'when master server is down' do before do - replica_connection.stubs(:send) - master_connection.stubs(:send).raises(ActiveRecord::StatementInvalid.new('PG::UnableToSend')) - master_connection.stubs(:reconnect!) - master_connection.stubs(:active?).returns(false) + @replica_connection = mock('replica_connection') - @old_const = described_class::HEARTBEAT_INTERVAL - described_class.const_set("HEARTBEAT_INTERVAL", 0.1) + ActiveRecord::Base.expects(:postgresql_connection).with(config).raises(PG::ConnectionBad) + + ActiveRecord::Base.expects(:postgresql_connection).with(config.merge({ + "host" => "localhost", "port" => "6432" + })).returns(@replica_connection) + + ActiveRecord::Base.expects(:verify_replica).with(@replica_connection) + + @replica_connection.expects(:disconnect!) + + ActiveRecord::Base.stubs(:interval).returns(0.1) + + Concurrent::TimerTask.any_instance.expects(:shutdown) end - after do - Discourse.disable_readonly_mode - described_class.const_set("HEARTBEAT_INTERVAL", @old_const) - end + it 'should failover to a replica server' do + ActiveRecord::Base.postgresql_fallback_connection(config) - it "should set site to readonly mode and carry out failover and switch back procedures" do - expect(adapter.main_connection).to eq(master_connection) - adapter.proxy_method('some method') expect(Discourse.readonly_mode?).to eq(true) - expect(adapter.main_connection).to eq(replica_connection) - master_connection.stubs(:active?).returns(true) + ActiveRecord::Base.unstub(:postgresql_connection) sleep 0.15 expect(Discourse.readonly_mode?).to eq(false) - expect(adapter.main_connection).to eq(master_connection) + + expect(ActiveRecord::Base.connection) + .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) end end - it 'should raise errors not related to the database connection' do - master_connection.stubs(:send).raises(StandardError.new) - expect { adapter.proxy_method('some method') }.to raise_error(StandardError) - end + context 'when both master and replica server is down' do + it 'should raise the right error' do + ActiveRecord::Base.expects(:postgresql_connection).raises(PG::ConnectionBad).twice - it 'should proxy methods successfully' do - expect(adapter.proxy_method(:execute, 'SELECT 1').values[0][0]).to eq("1") - expect(adapter.proxy_method(:active?)).to eq(true) - expect(adapter.proxy_method(:raw_connection)).to eq(master_connection.raw_connection) + expect { ActiveRecord::Base.postgresql_fallback_connection(config) } + .to raise_error(PG::ConnectionBad) + end end end end From a08496bb1a952702a259ffe19deb3d8cef1b41a3 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 26 Jan 2016 18:03:49 +0800 Subject: [PATCH 011/140] Remove Concurrent::TimerTask which spawns a long lasting Thread. --- .../postgresql_fallback_adapter.rb | 108 ++++++++++++------ .../postgresql_fallback_adapter_spec.rb | 61 ++++++---- 2 files changed, 110 insertions(+), 59 deletions(-) diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index df5a23b9e9..a56f187b97 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -1,45 +1,86 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/postgresql_adapter' require 'discourse' -require 'concurrent' -class TaskObserver - def update(time, result, ex) - if result - logger.info { "PG connection heartbeat successfully returned #{result}" } - elsif ex.is_a?(Concurrent::TimeoutError) - logger.warning { "PG connection heartbeat timed out".freeze } - else - if ex.message.include?("PG::UnableToSend") - logger.info { "PG connection heartbeat: Master connection is not active.".freeze } - else - logger.error { "PG connection heartbeat failed with error: \"#{ex}\"" } - end +class PostgreSQLFallbackHandler + include Singleton + + attr_reader :running + attr_accessor :master + + def initialize + @master = true + @running = false + end + + def verify_master + return if @running && recently_checked? + @running = true + + Thread.new do + begin + logger.info "#{self.class}: Checking master server..." + connection = ActiveRecord::Base.postgresql_connection(config) + + if connection.active? + logger.info "#{self.class}: Master server is active. Reconnecting..." + ActiveRecord::Base.remove_connection + ActiveRecord::Base.establish_connection(config) + Discourse.disable_readonly_mode + @master = true + end + rescue => e + if e.message.include?("could not connect to server") + logger.warn "#{self.class}: Connection to master PostgreSQL server failed with '#{e.message}'" + else + raise e + end + ensure + @last_check = Time.zone.now + @running = false + end end end private + def config + ActiveRecord::Base.configurations[Rails.env] + end + def logger Rails.logger end + + def recently_checked? + if @last_check + Time.zone.now <= @last_check + 5.seconds + else + false + end + end end module ActiveRecord module ConnectionHandling def postgresql_fallback_connection(config) - begin - connection = postgresql_connection(config) - rescue PG::ConnectionBad => e + fallback_handler = ::PostgreSQLFallbackHandler.instance + config = config.symbolize_keys + + if !fallback_handler.master connection = postgresql_connection(config.dup.merge({ - "host" => config["replica_host"], "port" => config["replica_port"] + host: config[:replica_host], port: config[:replica_port] })) verify_replica(connection) - - Discourse.enable_readonly_mode if !Discourse.readonly_mode? - - start_connection_heartbeart(connection, config) + Discourse.enable_readonly_mode + else + begin + connection = postgresql_connection(config) + rescue PG::ConnectionBad => e + fallback_handler.master = false + raise e + end end connection @@ -51,24 +92,23 @@ module ActiveRecord value = connection.raw_connection.exec("SELECT pg_is_in_recovery()").values[0][0] raise "Replica database server is not in recovery mode." if value == 'f' end + end - def interval - 5 - end + module ConnectionAdapters + class PostgreSQLAdapter + set_callback :checkout, :before, :switch_back? - def start_connection_heartbeart(existing_connection, config) - timer_task = Concurrent::TimerTask.new(execution_interval: interval) do |task| - connection = postgresql_connection(config) + private - if connection.active? - existing_connection.disconnect! - Discourse.disable_readonly_mode if Discourse.readonly_mode? - task.shutdown - end + def fallback_handler + @fallback_handler ||= ::PostgreSQLFallbackHandler.instance end - timer_task.add_observer(TaskObserver.new) - timer_task.execute + def switch_back? + if !fallback_handler.master && !fallback_handler.running + fallback_handler.verify_master + end + end end end end diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index 66dd0707c0..05013407d6 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -7,12 +7,12 @@ describe ActiveRecord::ConnectionHandling do "adapter" => "postgresql_fallback", "replica_host" => "localhost", "replica_port" => "6432" - }) + }).symbolize_keys! end after do - ActiveRecord::Base.clear_all_connections! Discourse.disable_readonly_mode + ::PostgreSQLFallbackHandler.instance.master = true end describe "#postgresql_fallback_connection" do @@ -24,34 +24,43 @@ describe ActiveRecord::ConnectionHandling do context 'when master server is down' do before do @replica_connection = mock('replica_connection') - - ActiveRecord::Base.expects(:postgresql_connection).with(config).raises(PG::ConnectionBad) - - ActiveRecord::Base.expects(:postgresql_connection).with(config.merge({ - "host" => "localhost", "port" => "6432" - })).returns(@replica_connection) - - ActiveRecord::Base.expects(:verify_replica).with(@replica_connection) - - @replica_connection.expects(:disconnect!) - - ActiveRecord::Base.stubs(:interval).returns(0.1) - - Concurrent::TimerTask.any_instance.expects(:shutdown) end it 'should failover to a replica server' do - ActiveRecord::Base.postgresql_fallback_connection(config) + begin + ActiveRecord::Base.expects(:postgresql_connection).with(config).raises(PG::ConnectionBad) + ActiveRecord::Base.expects(:verify_replica).with(@replica_connection) - expect(Discourse.readonly_mode?).to eq(true) + ActiveRecord::Base.expects(:postgresql_connection).with(config.merge({ + host: "localhost", port: "6432" + })).returns(@replica_connection) - ActiveRecord::Base.unstub(:postgresql_connection) - sleep 0.15 + expect { ActiveRecord::Base.postgresql_fallback_connection(config) } + .to raise_error(PG::ConnectionBad) - expect(Discourse.readonly_mode?).to eq(false) + expect{ ActiveRecord::Base.postgresql_fallback_connection(config) } + .to change{ Discourse.readonly_mode? }.from(false).to(true) - expect(ActiveRecord::Base.connection) - .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) + ActiveRecord::Base.unstub(:postgresql_connection) + + current_threads = Thread.list + + expect{ ActiveRecord::Base.connection_pool.checkout } + .to change{ Thread.list.size }.by(1) + + # Wait for the thread to finish execution + threads = (Thread.list - current_threads).each(&:join) + + expect(Discourse.readonly_mode?).to eq(false) + + expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0) + + expect(ActiveRecord::Base.connection) + .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) + ensure + # threads.each { |t| Thread.kill(t) } if threads + ActiveRecord::Base.establish_connection(:test) + end end end @@ -59,8 +68,10 @@ describe ActiveRecord::ConnectionHandling do it 'should raise the right error' do ActiveRecord::Base.expects(:postgresql_connection).raises(PG::ConnectionBad).twice - expect { ActiveRecord::Base.postgresql_fallback_connection(config) } - .to raise_error(PG::ConnectionBad) + 2.times do + expect { ActiveRecord::Base.postgresql_fallback_connection(config) } + .to raise_error(PG::ConnectionBad) + end end end end From c532d7d1ae4c353f58557374fc6371065e1b7910 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 4 Feb 2016 11:06:02 +0800 Subject: [PATCH 012/140] Internally `AR::Base.establish_connection` removes the current connection. --- .../postgresql_fallback_adapter.rb | 1 - .../postgresql_fallback_adapter_spec.rb | 43 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index a56f187b97..964955510f 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -24,7 +24,6 @@ class PostgreSQLFallbackHandler if connection.active? logger.info "#{self.class}: Master server is active. Reconnecting..." - ActiveRecord::Base.remove_connection ActiveRecord::Base.establish_connection(config) Discourse.disable_readonly_mode @master = true diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index 05013407d6..c693fe4693 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -27,40 +27,35 @@ describe ActiveRecord::ConnectionHandling do end it 'should failover to a replica server' do - begin - ActiveRecord::Base.expects(:postgresql_connection).with(config).raises(PG::ConnectionBad) - ActiveRecord::Base.expects(:verify_replica).with(@replica_connection) + ActiveRecord::Base.expects(:postgresql_connection).with(config).raises(PG::ConnectionBad) + ActiveRecord::Base.expects(:verify_replica).with(@replica_connection) - ActiveRecord::Base.expects(:postgresql_connection).with(config.merge({ - host: "localhost", port: "6432" - })).returns(@replica_connection) + ActiveRecord::Base.expects(:postgresql_connection).with(config.merge({ + host: "localhost", port: "6432" + })).returns(@replica_connection) - expect { ActiveRecord::Base.postgresql_fallback_connection(config) } - .to raise_error(PG::ConnectionBad) + expect { ActiveRecord::Base.postgresql_fallback_connection(config) } + .to raise_error(PG::ConnectionBad) - expect{ ActiveRecord::Base.postgresql_fallback_connection(config) } - .to change{ Discourse.readonly_mode? }.from(false).to(true) + expect{ ActiveRecord::Base.postgresql_fallback_connection(config) } + .to change{ Discourse.readonly_mode? }.from(false).to(true) - ActiveRecord::Base.unstub(:postgresql_connection) + ActiveRecord::Base.unstub(:postgresql_connection) - current_threads = Thread.list + current_threads = Thread.list - expect{ ActiveRecord::Base.connection_pool.checkout } - .to change{ Thread.list.size }.by(1) + expect{ ActiveRecord::Base.connection_pool.checkout } + .to change{ Thread.list.size }.by(1) - # Wait for the thread to finish execution - threads = (Thread.list - current_threads).each(&:join) + # Wait for the thread to finish execution + threads = (Thread.list - current_threads).each(&:join) - expect(Discourse.readonly_mode?).to eq(false) + expect(Discourse.readonly_mode?).to eq(false) - expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0) + expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0) - expect(ActiveRecord::Base.connection) - .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) - ensure - # threads.each { |t| Thread.kill(t) } if threads - ActiveRecord::Base.establish_connection(:test) - end + expect(ActiveRecord::Base.connection) + .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) end end From 886273f158bb677e924eedaf5a314b16fd51b742 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 5 Feb 2016 13:05:47 +1100 Subject: [PATCH 013/140] FIX: when CDN assets are not in root path source maps fail --- lib/global_path.rb | 9 +++++++++ lib/tasks/assets.rake | 4 ++-- spec/components/global_path_spec.rb | 30 +++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 spec/components/global_path_spec.rb diff --git a/lib/global_path.rb b/lib/global_path.rb index 20f09f18bd..c2d9a2a4da 100644 --- a/lib/global_path.rb +++ b/lib/global_path.rb @@ -6,4 +6,13 @@ module GlobalPath def cdn_path(p) "#{GlobalSetting.cdn_url}#{path(p)}" end + + def cdn_relative_path(path) + if (cdn_url = GlobalSetting.cdn_url).present? + URI.parse(cdn_url).path + path + else + path + end + end + end diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index 040f70e2b2..ff109fbc28 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -102,8 +102,8 @@ end def compress_node(from,to) to_path = "#{assets_path}/#{to}" - - source_map_root = (d=File.dirname(from)) == "." ? "/assets" : "/assets/#{d}" + assets = cdn_relative_path("/assets") + source_map_root = assets + (d=File.dirname(from)) == "." ? "" : "/#{d}" source_map_url = cdn_path "/assets/#{to}.map" cmd = "uglifyjs '#{assets_path}/#{from}' -p relative -c -m -o '#{to_path}' --source-map-root '#{source_map_root}' --source-map '#{assets_path}/#{to}.map' --source-map-url '#{source_map_url}'" diff --git a/spec/components/global_path_spec.rb b/spec/components/global_path_spec.rb new file mode 100644 index 0000000000..41b778c9af --- /dev/null +++ b/spec/components/global_path_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' +require 'global_path' + +class GlobalPathInstance + extend GlobalPath +end + +describe GlobalPath do + + context 'cdn_relative_path' do + def cdn_relative_path(p) + GlobalPathInstance.cdn_relative_path(p) + end + + it "just returns path for no cdn" do + expect(cdn_relative_path("/test")).to eq("/test") + end + + it "returns path when a cdn is defined with a path" do + GlobalSetting.expects(:cdn_url).returns("//something.com/foo") + expect(cdn_relative_path("/test")).to eq("/foo/test") + end + + it "returns path when a cdn is defined with a path" do + GlobalSetting.expects(:cdn_url).returns("https://something.com:221/foo") + expect(cdn_relative_path("/test")).to eq("/foo/test") + end + + end +end From 74dc838f5f7b7e7c9964e752598212066b0fab7a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 5 Feb 2016 10:33:35 +0800 Subject: [PATCH 014/140] FIX: Add a lock to ensure only a single thread is running each time. --- .../postgresql_fallback_adapter.rb | 12 ++++++++---- .../postgresql_fallback_adapter_spec.rb | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index 964955510f..b1f88e372d 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -11,11 +11,14 @@ class PostgreSQLFallbackHandler def initialize @master = true @running = false + @mutex = Mutex.new end def verify_master - return if @running && recently_checked? - @running = true + @mutex.synchronize do + return if @running || recently_checked? + @running = true + end Thread.new do begin @@ -23,6 +26,7 @@ class PostgreSQLFallbackHandler connection = ActiveRecord::Base.postgresql_connection(config) if connection.active? + connection.disconnect! logger.info "#{self.class}: Master server is active. Reconnecting..." ActiveRecord::Base.establish_connection(config) Discourse.disable_readonly_mode @@ -53,7 +57,7 @@ class PostgreSQLFallbackHandler def recently_checked? if @last_check - Time.zone.now <= @last_check + 5.seconds + Time.zone.now <= (@last_check + 5.seconds) else false end @@ -66,7 +70,7 @@ module ActiveRecord fallback_handler = ::PostgreSQLFallbackHandler.instance config = config.symbolize_keys - if !fallback_handler.master + if !fallback_handler.master && !fallback_handler.running connection = postgresql_connection(config.dup.merge({ host: config[:replica_host], port: config[:replica_port] })) diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index c693fe4693..35a3487d26 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -2,11 +2,14 @@ require 'rails_helper' require_dependency 'active_record/connection_adapters/postgresql_fallback_adapter' describe ActiveRecord::ConnectionHandling do + let(:replica_host) { "1.1.1.1" } + let(:replica_port) { "6432" } + let(:config) do ActiveRecord::Base.configurations["test"].merge({ "adapter" => "postgresql_fallback", - "replica_host" => "localhost", - "replica_port" => "6432" + "replica_host" => replica_host, + "replica_port" => replica_port }).symbolize_keys! end @@ -31,7 +34,7 @@ describe ActiveRecord::ConnectionHandling do ActiveRecord::Base.expects(:verify_replica).with(@replica_connection) ActiveRecord::Base.expects(:postgresql_connection).with(config.merge({ - host: "localhost", port: "6432" + host: replica_host, port: replica_port })).returns(@replica_connection) expect { ActiveRecord::Base.postgresql_fallback_connection(config) } @@ -47,6 +50,14 @@ describe ActiveRecord::ConnectionHandling do expect{ ActiveRecord::Base.connection_pool.checkout } .to change{ Thread.list.size }.by(1) + # Ensure that we don't try to connect back to the replica when a thread + # is running + begin + ActiveRecord::Base.postgresql_fallback_connection(config) + rescue PG::ConnectionBad => e + # This is expected if the thread finishes before the above is called. + end + # Wait for the thread to finish execution threads = (Thread.list - current_threads).each(&:join) From 0032047804663992e7f8f7349dd40f7660d25e91 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 5 Feb 2016 14:59:33 +1100 Subject: [PATCH 015/140] missing a bracket --- lib/tasks/assets.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index ff109fbc28..ed695bc7da 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -103,7 +103,7 @@ end def compress_node(from,to) to_path = "#{assets_path}/#{to}" assets = cdn_relative_path("/assets") - source_map_root = assets + (d=File.dirname(from)) == "." ? "" : "/#{d}" + source_map_root = assets + ((d=File.dirname(from)) == "." ? "" : "/#{d}") source_map_url = cdn_path "/assets/#{to}.map" cmd = "uglifyjs '#{assets_path}/#{from}' -p relative -c -m -o '#{to_path}' --source-map-root '#{source_map_root}' --source-map '#{assets_path}/#{to}.map' --source-map-url '#{source_map_url}'" From 726d81f83b321acb032b009e2e01b8a13f8a22c4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 5 Feb 2016 16:47:47 +0800 Subject: [PATCH 016/140] FIX: Don't update autocomplete when removing the key. --- app/assets/javascripts/discourse/lib/autocomplete.js.es6 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 5aa01e495b..aeb2ffb1db 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -430,6 +430,10 @@ export default function(options) { term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition); + if ((completeStart === caretPosition) && (term === options.key)) { + closeAutocomplete(); + } + updateAutoComplete(options.dataSource(term)); return true; default: From e3747f654b9b09898d68210cb9b219d2959a68fb Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 6 Feb 2016 01:02:48 +1100 Subject: [PATCH 017/140] SECURITY: hoist blocks using guids, not md5 hashes --- .../dialects/bold_italics_dialect.js | 4 +-- .../javascripts/discourse/dialects/dialect.js | 27 ++++++++++++++----- app/assets/javascripts/vendor.js | 1 - lib/pretty_text.rb | 1 - 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js index cddc1b8c78..05d0178d35 100644 --- a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js +++ b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js @@ -1,5 +1,3 @@ -/* global md5:true */ - /** markdown-js doesn't ensure that em/strong codes are present on word boundaries. So we create our own handlers here. @@ -34,7 +32,7 @@ var unhoist = function(obj,from,to){ }; var replaceMarkdown = function(match, tag) { - var hash = md5(match[0]); + var hash = Discourse.Dialect.guid(); Discourse.Dialect.registerInline(match, function(text, matched, prev){ if(!text || text.length < match.length + 1) { diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js index 760bddd046..f53bb58ca5 100644 --- a/app/assets/javascripts/discourse/dialects/dialect.js +++ b/app/assets/javascripts/discourse/dialects/dialect.js @@ -1,4 +1,3 @@ -/*global md5:true */ /** Discourse uses the Markdown.js as its main parser. `Discourse.Dialect` is the framework @@ -44,7 +43,7 @@ function processTextNodes(node, event, emitter) { if (node.length < 2) { return; } if (node[0] === '__RAW') { - var hash = md5(node[1]); + var hash = Discourse.Dialect.guid(); hoisted[hash] = node[1]; node[1] = hash; return; @@ -156,7 +155,7 @@ function countLines(str) { function hoister(t, target, replacement) { var regexp = new RegExp(target.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), "g"); if (t.match(regexp)) { - var hash = md5(target); + var hash = Discourse.Dialect.guid(); t = t.replace(regexp, hash); hoisted[hash] = replacement; } @@ -190,7 +189,7 @@ function hoistCodeBlocksAndSpans(text) { // fenced code blocks (AKA GitHub code blocks) text = text.replace(/(^\n*|\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) { - var hash = md5(content); + var hash = Discourse.Dialect.guid(); hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content))); return before + "```" + language + "\n" + hash + "\n```"; }); @@ -206,14 +205,14 @@ function hoistCodeBlocksAndSpans(text) { } } // we can safely hoist the code block - var hash = md5(content); + var hash = Discourse.Dialect.guid(); hoisted[hash] = escape(outdent(showBackslashEscapedCharacters(removeEmptyLines(content)))); return before + " " + hash + "\n"; }); //
...
code blocks text = text.replace(/(\s|^)
([\s\S]*?)<\/pre>/ig, function(_, before, content) {
-    var hash = md5(content);
+    var hash = Discourse.Dialect.guid();
     hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
     return before + "
" + hash + "
"; }); @@ -222,7 +221,7 @@ function hoistCodeBlocksAndSpans(text) { ["``", "`"].forEach(function(delimiter) { var regexp = new RegExp("(^|[^`])" + delimiter + "([^`\\n]+?)" + delimiter + "([^`]|$)", "g"); text = text.replace(regexp, function(_, before, content, after) { - var hash = md5(content); + var hash = Discourse.Dialect.guid(); hoisted[hash] = escape(showBackslashEscapedCharacters(content.trim())); return before + delimiter + hash + delimiter + after; }); @@ -241,6 +240,20 @@ function hoistCodeBlocksAndSpans(text) { **/ Discourse.Dialect = { + // http://stackoverflow.com/a/8809472/17174 + guid: function(){ + var d = new Date().getTime(); + if(window.performance && typeof window.performance.now === "function"){ + d += performance.now(); //use high-precision timer if available + } + var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = (d + Math.random()*16)%16 | 0; + d = Math.floor(d/16); + return (c=='x' ? r : (r&0x3|0x8)).toString(16); + }); + return uuid; + }, + /** Cook text using the dialects. diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index 29ee2c5328..e63bf8031e 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -29,7 +29,6 @@ //= require jquery.tagsinput.js //= require jquery.sortable.js //= require lodash.js -//= require md5.js //= require modernizr.custom.00874.js //= require mousetrap.js //= require rsvp.js diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index eb9f079d77..f300d56de2 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -87,7 +87,6 @@ module PrettyText ctx["helpers"] = Helpers.new ctx_load(ctx, - "vendor/assets/javascripts/md5.js", "vendor/assets/javascripts/lodash.js", "vendor/assets/javascripts/Markdown.Converter.js", "lib/headless-ember.js", From 56a16a0e8910a109ed5999d3a4a20f09cef4f244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 5 Feb 2016 15:27:24 +0100 Subject: [PATCH 018/140] we still need md5 --- app/assets/javascripts/vendor.js | 1 + lib/pretty_text.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index e63bf8031e..29ee2c5328 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -29,6 +29,7 @@ //= require jquery.tagsinput.js //= require jquery.sortable.js //= require lodash.js +//= require md5.js //= require modernizr.custom.00874.js //= require mousetrap.js //= require rsvp.js diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index f300d56de2..eb9f079d77 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -87,6 +87,7 @@ module PrettyText ctx["helpers"] = Helpers.new ctx_load(ctx, + "vendor/assets/javascripts/md5.js", "vendor/assets/javascripts/lodash.js", "vendor/assets/javascripts/Markdown.Converter.js", "lib/headless-ember.js", From 91ec2c51718daa4f1fa9b182ff7ee469b113f5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 5 Feb 2016 16:08:31 +0100 Subject: [PATCH 019/140] fix eslint --- app/assets/javascripts/discourse/dialects/dialect.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js index f53bb58ca5..7cf55dba26 100644 --- a/app/assets/javascripts/discourse/dialects/dialect.js +++ b/app/assets/javascripts/discourse/dialects/dialect.js @@ -1,3 +1,5 @@ +/*eslint no-bitwise:0 */ + /** Discourse uses the Markdown.js as its main parser. `Discourse.Dialect` is the framework @@ -247,9 +249,9 @@ Discourse.Dialect = { d += performance.now(); //use high-precision timer if available } var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = (d + Math.random()*16)%16 | 0; + var r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d/16); - return (c=='x' ? r : (r&0x3|0x8)).toString(16); + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); return uuid; }, From 0aa59956fa3aa7252fb2ace08256a8470e2848e8 Mon Sep 17 00:00:00 2001 From: Devon Estes Date: Fri, 5 Feb 2016 16:16:33 +0100 Subject: [PATCH 020/140] Extract method refactoring in Jobs::ExportCsvFile I was combing through some of the files with worse grades on Code Climate as a guide for places where I could jump in and help, and I saw this as one of the ones in need of some love. I reduced duplication in the #user_list_export method by extracting several methods that were common to both branches of the logic in that method. --- app/jobs/regular/export_csv_file.rb | 81 ++++++++++++++--------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index bacf8c6f70..1f955f574e 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -44,7 +44,7 @@ module Jobs end def user_archive_export - user_archive_data = Post.includes(:topic => :category).where(user_id: @current_user.id).select('topic_id','post_number','raw','like_count','reply_count','created_at').order('created_at').with_deleted.to_a + user_archive_data = Post.includes(:topic => :category).where(user_id: @current_user.id).select(:topic_id, :post_number, :raw, :like_count, :reply_count, :created_at).order(:created_at).with_deleted.to_a user_archive_data.map do |user_archive| get_user_archive_fields(user_archive) end @@ -57,53 +57,19 @@ module Jobs if SiteSetting.enable_sso # SSO enabled User.includes(:user_stat, :single_sign_on_record, :groups).find_each do |user| - user_info_string = "#{user.id},#{user.name},#{user.username},#{user.email},#{user.title},#{user.created_at},#{user.last_seen_at},#{user.last_posted_at},#{user.last_emailed_at},#{user.trust_level},#{user.approved},#{user.suspended_at},#{user.suspended_till},#{user.blocked},#{user.active},#{user.admin},#{user.moderator},#{user.ip_address},#{user.user_stat.topics_entered},#{user.user_stat.posts_read_count},#{user.user_stat.time_read},#{user.user_stat.topic_count},#{user.user_stat.post_count},#{user.user_stat.likes_given},#{user.user_stat.likes_received}" - - # sso - if user.single_sign_on_record - user_info_string << ",#{user.single_sign_on_record.external_id},#{user.single_sign_on_record.external_email},#{user.single_sign_on_record.external_username},#{user.single_sign_on_record.external_name},#{user.single_sign_on_record.external_avatar_url}" - else - user_info_string << ",nil,nil,nil,nil,nil" - end - - # custom fields - if user_field_ids.present? - user.user_fields.each do |custom_field| - user_info_string << ",#{custom_field[1]}" - end - end - - # group names - group_names = "" - user.groups.each do |group| - group_names << "#{group.name};" - end - user_info_string << ",#{group_names[0..-2]}" unless group_names.blank? - group_names = nil - + user_info_string = get_base_user_string(user) + user_info_string = add_single_sign_on(user, user_info_string) + user_info_string = add_custom_fields(user, user_info_string, user_field_ids) + user_info_string = add_group_names(user, user_info_string) user_array.push(user_info_string.split(",")) user_info_string = nil end else # SSO disabled User.includes(:user_stat, :groups).find_each do |user| - user_info_string = "#{user.id},#{user.name},#{user.username},#{user.email},#{user.title},#{user.created_at},#{user.last_seen_at},#{user.last_posted_at},#{user.last_emailed_at},#{user.trust_level},#{user.approved},#{user.suspended_at},#{user.suspended_till},#{user.blocked},#{user.active},#{user.admin},#{user.moderator},#{user.ip_address},#{user.user_stat.topics_entered},#{user.user_stat.posts_read_count},#{user.user_stat.time_read},#{user.user_stat.topic_count},#{user.user_stat.post_count},#{user.user_stat.likes_given},#{user.user_stat.likes_received}" - - # custom fields - if user_field_ids.present? - user.user_fields.each do |custom_field| - user_info_string << ",#{custom_field[1]}" - end - end - - # group names - group_names = "" - user.groups.each do |group| - group_names << "#{group.name};" - end - user_info_string << ",#{group_names[0..-2]}" unless group_names.blank? - group_names = nil - + user_info_string = get_base_user_string(user) + user_info_string = add_custom_fields(user, user_info_string, user_field_ids) + user_info_string = add_group_names(user, user_info_string) user_array.push(user_info_string.split(",")) user_info_string = nil end @@ -182,6 +148,37 @@ module Jobs private + def get_base_user_string(user) + "#{user.id},#{user.name},#{user.username},#{user.email},#{user.title},#{user.created_at},#{user.last_seen_at},#{user.last_posted_at},#{user.last_emailed_at},#{user.trust_level},#{user.approved},#{user.suspended_at},#{user.suspended_till},#{user.blocked},#{user.active},#{user.admin},#{user.moderator},#{user.ip_address},#{user.user_stat.topics_entered},#{user.user_stat.posts_read_count},#{user.user_stat.time_read},#{user.user_stat.topic_count},#{user.user_stat.post_count},#{user.user_stat.likes_given},#{user.user_stat.likes_received}" + end + + def add_single_sign_on(user, user_info_string) + if user.single_sign_on_record + user_info_string << ",#{user.single_sign_on_record.external_id},#{user.single_sign_on_record.external_email},#{user.single_sign_on_record.external_username},#{user.single_sign_on_record.external_name},#{user.single_sign_on_record.external_avatar_url}" + else + user_info_string << ",nil,nil,nil,nil,nil" + end + user_info_string + end + + def add_custom_fields(user, user_info_string, user_field_ids) + if user_field_ids.present? + user.user_fields.each do |custom_field| + user_info_string << ",#{custom_field[1]}" + end + end + user_info_string + end + + def add_group_names(user, user_info_string) + group_names = user.groups.each_with_object("") do |group, names| + names << "#{group.name};" + end + user_info_string << ",#{group_names[0..-2]}" unless group_names.blank? + group_names = nil + user_info_string + end + def get_user_archive_fields(user_archive) user_archive_array = [] topic_data = user_archive.topic From ea0e63b150f8a8baea62ec86e295efe80368c16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 5 Feb 2016 20:07:30 +0100 Subject: [PATCH 021/140] FIX: handle cases where we only pass the notification type rather than the notification id when sending user email --- app/jobs/regular/user_email.rb | 15 ++++++++------- spec/jobs/user_email_spec.rb | 3 +++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/jobs/regular/user_email.rb b/app/jobs/regular/user_email.rb index 54a18809ed..14f34b42c2 100644 --- a/app/jobs/regular/user_email.rb +++ b/app/jobs/regular/user_email.rb @@ -48,13 +48,7 @@ module Jobs @skip_context = { type: type, user_id: user_id, to_address: to_address } end - NOTIFICATIONS_SENT_BY_MAILING_LIST ||= Set.new [ - Notification.types[:posted], - Notification.types[:replied], - Notification.types[:mentioned], - Notification.types[:group_mentioned], - Notification.types[:quoted], - ] + NOTIFICATIONS_SENT_BY_MAILING_LIST ||= Set.new %w{posted replied mentioned group_mentioned quoted} def message_for_email(user, post, type, notification, notification_type=nil, notification_data_hash=nil, @@ -84,6 +78,13 @@ module Jobs email_args[:notification_type] ||= notification_type || notification.try(:notification_type) email_args[:notification_data_hash] ||= notification_data_hash || notification.try(:data_hash) + unless String === email_args[:notification_type] + if Numeric === email_args[:notification_type] + email_args[:notification_type] = Notification.types[email_args[:notification_type]] + end + email_args[:notification_type] = email_args[:notification_type].to_s + end + if user.mailing_list_mode? && !post.topic.private_message? && NOTIFICATIONS_SENT_BY_MAILING_LIST.include?(email_args[:notification_type]) diff --git a/spec/jobs/user_email_spec.rb b/spec/jobs/user_email_spec.rb index e0dc712d65..c7ab345a11 100644 --- a/spec/jobs/user_email_spec.rb +++ b/spec/jobs/user_email_spec.rb @@ -195,7 +195,10 @@ describe Jobs::UserEmail do it "doesn't send the mail if the user is using mailing list mode" do Email::Sender.any_instance.expects(:send).never user.update_column(:mailing_list_mode, true) + # sometimes, we pass the notification_id Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id) + # other times, we only pass the type of notification + Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_type: "posted", post_id: post.id) end it "doesn't send the email if the post has been user deleted" do From 4cb6d2b0ec2ccffd1af6c1fc17936f2d2721deaf Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Tue, 26 Jan 2016 17:07:54 +0100 Subject: [PATCH 022/140] Updating Discuz import script (Most work done by zh99998) --- script/import_scripts/discuz_x.rb | 488 ++++++++++++++++++++++++------ 1 file changed, 402 insertions(+), 86 deletions(-) diff --git a/script/import_scripts/discuz_x.rb b/script/import_scripts/discuz_x.rb index f252ddb861..791b6c38da 100644 --- a/script/import_scripts/discuz_x.rb +++ b/script/import_scripts/discuz_x.rb @@ -7,6 +7,8 @@ # This script is tested only on Simplified Chinese Discuz! X instances # If you want to import data other than Simplified Chinese, email me. +require 'php_serialize' +require 'miro' require 'mysql2' require File.expand_path(File.dirname(__FILE__) + "/base.rb") @@ -34,9 +36,23 @@ class ImportScripts::DiscuzX < ImportScripts::Base database: DISCUZX_DB ) @first_post_id_by_topic_id = {} + + @internal_url_regexps = [ + /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/forum\.php\?mod=viewthread(?:&|&)tid=(?\d+)(?:[^\[\]\s]*)(?:pid=?(?\d+))?(?:[^\[\]\s]*)/, + /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/viewthread\.php\?tid=(?\d+)(?:[^\[\]\s]*)(?:pid=?(?\d+))?(?:[^\[\]\s]*)/, + /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/forum\.php\?mod=redirect(?:&|&)goto=findpost(?:&|&)pid=(?\d+)(?:&|&)ptid=(?\d+)(?:[^\[\]\s]*)/, + /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/redirect\.php\?goto=findpost(?:&|&)pid=(?\d+)(?:&|&)ptid=(?\d+)(?:[^\[\]\s]*)/, + /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/forumdisplay\.php\?fid=(?\d+)(?:[^\[\]\s]*)/, + /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/forum\.php\?mod=forumdisplay(?:&|&)fid=(?\d+)(?:[^\[\]\s]*)/, + /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/(?index)\.php(?:[^\[\]\s]*)/, + /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/(?stats)\.php(?:[^\[\]\s]*)/, + /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/misc.php\?mod=(?stat|ranklist)(?:[^\[\]\s]*)/ + ] + end def execute + get_knowledge_about_duplicated_email import_users import_categories import_posts @@ -53,19 +69,53 @@ class ImportScripts::DiscuzX < ImportScripts::Base def get_knowledge_about_group group_table = table_name 'common_usergroup' result = mysql_query( - "SELECT groupid group_id, radminid role_id, type, grouptitle title + "SELECT groupid group_id, radminid role_id FROM #{group_table};") - @moderator_group_id = -1 - @admin_group_id = -1 + @moderator_group_id = [] + @admin_group_id = [] + #@banned_group_id = [4,5] # 禁止的用户及其帖子均不导入,如果你想导入这些用户和帖子,请把这个数组清空。 result.each do |group| - role_id = group['role_id'] - group_id = group['group_id'] - case group['title'].strip - when '管理员' - @admin_admin_id = role_id - when '超级版主' - @moderator_admin_id = role_id + case group['role_id'] + when 1 # 管理员 + @admin_group_id << group['group_id'] + when 2, 3 # 超级版主、版主。如果你不希望原普通版主成为Discourse版主,把3去掉。 + @moderator_group_id << group['group_id'] + end + end + end + + def get_knowledge_about_category_slug + @category_slug = {} + results = mysql_query("SELECT svalue value + FROM #{table_name 'common_setting'} + WHERE skey = 'forumkeys'") + + return if results.size < 1 + value = results.first['value'] + + return if value.blank? + + PHP.unserialize(value).each do |category_import_id, slug| + next if slug.blank? + @category_slug[category_import_id] = slug + end + end + + def get_knowledge_about_duplicated_email + @duplicated_email = {} + results = mysql_query( + "select a.uid uid, b.uid import_id from pre_common_member a + join (select uid, email from pre_common_member group by email having count(email) > 1 order by uid asc) b USING(email) + where a.uid != b.uid") + + users = @lookup.instance_variable_get :@users + + results.each do |row| + @duplicated_email[row['uid']] = row['import_id'] + user_id = users[row['import_id']] + if user_id + users[row['uid']] = user_id end end end @@ -79,51 +129,63 @@ class ImportScripts::DiscuzX < ImportScripts::Base user_table = table_name 'common_member' profile_table = table_name 'common_member_profile' status_table = table_name 'common_member_status' + forum_table = table_name 'common_member_field_forum' + home_table = table_name 'common_member_field_home' total_count = mysql_query("SELECT count(*) count FROM #{user_table};").first['count'] batches(BATCH_SIZE) do |offset| results = mysql_query( - "SELECT u.uid id, u.username username, u.email email, u.adminid admin_id, su.regdate regdate, s.regip regip, - u.emailstatus email_confirmed, u.avatarstatus avatar_exists, p.site website, p.resideprovince province, - p.residecity city, p.residedist country, p.residecommunity community, p.residesuite apartment, - p.bio bio, s.lastip last_visit_ip, s.lastvisit last_visit_time, s.lastpost last_posted_at, - s.lastsendmail last_emailed_at + "SELECT u.uid id, u.username username, u.email email, u.groupid group_id, + su.regdate regdate, su.password password_hash, su.salt salt, + s.regip regip, s.lastip last_visit_ip, s.lastvisit last_visit_time, s.lastpost last_posted_at, s.lastsendmail last_emailed_at, + u.emailstatus email_confirmed, u.avatarstatus avatar_exists, + p.site website, p.address address, p.bio bio, p.realname realname, p.qq qq, + p.resideprovince resideprovince, p.residecity residecity, p.residedist residedist, p.residecommunity residecommunity, + p.resideprovince birthprovince, p.birthcity birthcity, p.birthdist birthdist, p.birthcommunity birthcommunity, + h.spacecss spacecss, h.spacenote spacenote, + f.customstatus customstatus, f.sightml sightml FROM #{user_table} u - JOIN #{sensitive_user_table} su ON su.uid = u.uid - JOIN #{profile_table} p ON p.uid = u.uid - JOIN #{status_table} s ON s.uid = u.uid + LEFT JOIN #{sensitive_user_table} su USING(uid) + LEFT JOIN #{profile_table} p USING(uid) + LEFT JOIN #{status_table} s USING(uid) + LEFT JOIN #{forum_table} f USING(uid) + LEFT JOIN #{home_table} h USING(uid) ORDER BY u.uid ASC LIMIT #{BATCH_SIZE} OFFSET #{offset};") break if results.size < 1 - next if all_records_exist? :users, users.map {|u| u["id"].to_i} + # TODO: breaks the scipt reported by some users + # next if all_records_exist? :users, users.map {|u| u["id"].to_i} create_users(results, total: total_count, offset: offset) do |user| { id: user['id'], email: user['email'], username: user['username'], - name: user['username'], - created_at: Time.zone.at(user['regdate']), + name: first_exists(user['realname'], user['customstatus'], user['username']), + import_pass: user['password_hash'], + active: true, + salt: user['salt'], + # TODO: title: user['customstatus'], # move custom title to name since discourse can't let user custom title https://meta.discourse.org/t/let-users-custom-their-title/37626 + created_at: user['regdate'] ? Time.zone.at(user['regdate']) : nil, registration_ip_address: user['regip'], ip_address: user['last_visit_ip'], last_seen_at: user['last_visit_time'], last_emailed_at: user['last_emailed_at'], last_posted_at: user['last_posted_at'], - moderator: user['admin_id'] == @moderator_admin_id, - admin: user['admin_id'] == @admin_admin_id, - active: true, - website: user['website'], - bio_raw: user['bio'], - location: "#{user['province']}#{user['city']}#{user['country']}#{user['community']}#{user['apartment']}", + moderator: @moderator_group_id.include?(user['group_id']), + admin: @admin_group_id.include?(user['group_id']), + website: (user['website'] and user['website'].include?('.')) ? user['website'].strip : ( user['qq'] and user['qq'].strip == user['qq'].strip.to_i and user['qq'].strip.to_i > 10000 ) ? 'http://user.qzone.qq.com/' + user['qq'].strip : nil, + bio_raw: first_exists((user['bio'] and CGI.unescapeHTML(user['bio'])), user['sightml'], user['spacenote']).strip[0,3000], + location: first_exists(user['address'], (!user['resideprovince'].blank? ? [user['resideprovince'], user['residecity'], user['residedist'], user['residecommunity']] : [user['birthprovince'], user['birthcity'], user['birthdist'], user['birthcommunity']]).reject{|location|location.blank?}.join(' ')), post_create_action: lambda do |newmember| if user['avatar_exists'] == 1 and newmember.uploaded_avatar_id.blank? path, filename = discuzx_avatar_fullpath(user['id']) if path begin upload = create_upload(newmember.id, path, filename) - if upload.persisted? + if !upload.nil? && upload.persisted? newmember.import_mode = false newmember.create_user_avatar newmember.import_mode = true @@ -137,9 +199,42 @@ class ImportScripts::DiscuzX < ImportScripts::Base end end end + if !user['spacecss'].blank? and newmember.user_profile.profile_background.blank? + # profile background + if matched = user['spacecss'].match(/body\s*{[^}]*url\('?(.+?)'?\)/i) + body_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last + end + if matched = user['spacecss'].match(/#hd\s*{[^}]*url\('?(.+?)'?\)/i) + header_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last + end + if matched = user['spacecss'].match(/.blocktitle\s*{[^}]*url\('?(.+?)'?\)/i) + blocktitle_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last + end + if matched = user['spacecss'].match(/#ct\s*{[^}]*url\('?(.+?)'?\)/i) + content_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last + end + + if body_background || header_background || blocktitle_background || content_background + profile_background = first_exists(header_background, body_background, content_background, blocktitle_background) + card_background = first_exists(content_background, body_background, header_background, blocktitle_background) + upload = create_upload(newmember.id, File.join(DISCUZX_BASE_DIR, profile_background), File.basename(profile_background)) + if upload + newmember.user_profile.upload_profile_background upload + else + puts "WARNING: #{user['username']} (UID: #{user['id']}) profile_background file did not persist!" + end + upload = create_upload(newmember.id, File.join(DISCUZX_BASE_DIR, card_background), File.basename(card_background)) + if upload + newmember.user_profile.upload_card_background upload + else + puts "WARNING: #{user['username']} (UID: #{user['id']}) card_background file did not persist!" + end + end + end # we don't send email to the unconfirmed user newmember.update(email_digests: user['email_confirmed'] == 1) if newmember.email_digests + newmember.update(name: '') if !newmember.name.blank? and newmember.name == newmember.username end } end @@ -149,27 +244,57 @@ class ImportScripts::DiscuzX < ImportScripts::Base def import_categories puts '', "creating categories" + get_knowledge_about_category_slug + forums_table = table_name 'forum_forum' forums_data_table = table_name 'forum_forumfield' results = mysql_query(" SELECT f.fid id, f.fup parent_id, f.name, f.type type, f.status status, f.displayorder position, - d.description description + d.description description, d.rules rules, d.icon, d.extra extra FROM #{forums_table} f - JOIN #{forums_data_table} d ON f.fid = d.fid + LEFT JOIN #{forums_data_table} d USING(fid) ORDER BY parent_id ASC, id ASC ") max_position = Category.all.max_by(&:position).position create_categories(results) do |row| - next if row['type'] == 'group' || row['status'].to_i == 3 + next if row['type'] == 'group' or row['status'] == 2 # or row['status'].to_i == 3 # 如果不想导入群组,取消注释 + extra = PHP.unserialize(row['extra']) if !row['extra'].blank? + if extra and !extra["namecolor"].blank? + color = extra["namecolor"][1,6] + end Category.all.max_by(&:position).position + h = { id: row['id'], name: row['name'], description: row['description'], - position: row['position'].to_i + max_position + position: row['position'].to_i + max_position, + color: color, + suppress_from_homepage: (row['status'] == 0 or row['status'] == 3), + post_create_action: lambda do |category| + if slug = @category_slug[row['id']] + category.update(slug: slug) + end + + raw = process_discuzx_post(row['rules'], nil) + if @bbcode_to_md + raw = raw.bbcode_to_md(false) rescue raw + end + category.topic.posts.first.update_attribute(:raw, raw) + if !row['icon'].empty? + upload = create_upload(Discourse::SYSTEM_USER_ID, File.join(DISCUZX_BASE_DIR, ATTACHMENT_DIR, '../common', row['icon']), File.basename(row['icon'])) + if upload + category.logo_url = upload.url + # FIXME: I don't know how to get '/shared' by script. May change to Rails.root + category.color = Miro::DominantColors.new(File.join('/shared', category.logo_url)).to_hex.first[1,6] if !color + category.save! + end + end + category + end } if row['parent_id'].to_i > 0 h[:parent_category_id] = category_id_from_imported_category_id(row['parent_id']) @@ -181,6 +306,7 @@ class ImportScripts::DiscuzX < ImportScripts::Base def import_posts puts "", "creating topics and posts" + users_table = table_name 'common_member' posts_table = table_name 'forum_post' topics_table = table_name 'forum_thread' @@ -195,16 +321,18 @@ class ImportScripts::DiscuzX < ImportScripts::Base p.authorid user_id, p.message raw, p.dateline post_time, - p.first is_first_post, - p.invisible status - FROM #{posts_table} p, - #{topics_table} t - WHERE p.tid = t.tid + p2.pid first_id, + p.invisible status, + t.special special + FROM #{posts_table} p + JOIN #{posts_table} p2 ON p2.first AND p2.tid = p.tid + JOIN #{topics_table} t ON t.tid = p.tid + where t.tid < 10000 ORDER BY id ASC, topic_id ASC LIMIT #{BATCH_SIZE} OFFSET #{offset}; ") - + # u.status != -1 AND u.groupid != 4 AND u.groupid != 5 用户未被锁定、禁访或禁言。在现实中的 Discuz 论坛,禁止的用户通常是广告机或驱逐的用户,这些不需要导入。 break if results.size < 1 next if all_records_exist? :posts, results.map {|p| p["id"].to_i} @@ -218,40 +346,95 @@ class ImportScripts::DiscuzX < ImportScripts::Base mapped[:raw] = process_discuzx_post(m['raw'], m['id']) mapped[:created_at] = Time.zone.at(m['post_time']) - if m['is_first_post'] == 1 + if m['id'] == m['first_id'] mapped[:category] = category_id_from_imported_category_id(m['category_id']) mapped[:title] = CGI.unescapeHTML(m['title']) - @first_post_id_by_topic_id[m['topic_id']] = m['id'] + + if m['special'] == 1 + results = mysql_query(" + SELECT multiple, maxchoices + FROM #{table_name 'forum_poll'} + WHERE tid = #{m['topic_id']}") + poll = results.first || {} + results = mysql_query(" + SELECT polloption + FROM #{table_name 'forum_polloption'} + WHERE tid = #{m['topic_id']} + ORDER BY displayorder") + if results.empty? + puts "WARNING: can't find poll options for topic #{m['topic_id']}, skip poll" + else + mapped[:raw].prepend "[poll#{poll['multiple'] ? ' type=multiple' : ''}#{poll['maxchoices'] > 0 ? " max=#{poll['maxchoices']}" : ''}]\n#{results.map{|option|'- ' + option['polloption']}.join("\n")}\n[/poll]\n" + end + end else - parent = topic_lookup_from_imported_post_id(@first_post_id_by_topic_id[m['topic_id']]) + parent = topic_lookup_from_imported_post_id(m['first_id']) if parent mapped[:topic_id] = parent[:topic_id] - post_id = post_id_from_imported_post_id(find_post_id_by_quote_number(m['raw']).to_i) - if (post = Post.find_by(id: post_id)) - mapped[:reply_to_post_number] = post.post_number + reply_post_import_id = find_post_id_by_quote_number(m['raw']) + if reply_post_import_id + post_id = post_id_from_imported_post_id(reply_post_import_id.to_i) + if (post = Post.find_by(id: post_id)) + if post.topic_id == mapped[:topic_id] + mapped[:reply_to_post_number] = post.post_number + else + puts "post #{m['id']} reply to another topic, skip reply" + end + else + puts "post #{m['id']} reply to not exists post #{reply_post_import_id}, skip reply" + end end else puts "Parent topic #{m['topic_id']} doesn't exist. Skipping #{m['id']}: #{m['title'][0..40]}" skip = true end + end - if [-5, -3, -1].include? m['status'] || mapped[:raw].blank? + if m['status'] & 1 == 1 || mapped[:raw].blank? mapped[:post_create_action] = lambda do |post| PostDestroyer.new(Discourse.system_user, post).perform_delete end - elsif m['status'] == -2# waiting for approve + elsif (m['status'] & 2) >> 1 == 1 # waiting for approve mapped[:post_create_action] = lambda do |post| PostAction.act(Discourse.system_user, post, 6, {take_action: false}) end end - skip ? nil : mapped end end end + def import_bookmarks + puts '', 'creating bookmarks' + favorites_table = table_name 'home_favorite' + posts_table = table_name 'forum_post' + + total_count = mysql_query("SELECT count(*) count FROM #{favorites_table} WHERE idtype = 'tid'").first['count'] + batches(BATCH_SIZE) do |offset| + results = mysql_query(" + SELECT p.pid post_id, f.uid user_id + FROM #{favorites_table} f + JOIN #{posts_table} p ON f.id = p.tid + WHERE f.idtype = 'tid' AND p.first + LIMIT #{BATCH_SIZE} + OFFSET #{offset};") + + break if results.size < 1 + + # next if all_records_exist? + + create_bookmarks(results, total: total_count, offset: offset) do |row| + { + user_id: row['user_id'], + post_id: row['post_id'] + } + end + end + end + + def import_private_messages puts '', 'creating private messages' @@ -285,7 +468,7 @@ class ImportScripts::DiscuzX < ImportScripts::Base break if results.size < 1 - next if all_records_exist? :posts, results.map {|m| "pm:#{m['id']}"} + # next if all_records_exist? :posts, results.map {|m| "pm:#{m['id']}"} create_posts(results, total: total_count, offset: offset) do |m| skip = false @@ -349,8 +532,9 @@ class ImportScripts::DiscuzX < ImportScripts::Base result.first['id'].to_s == pm_id.to_s end - def process_discuzx_post(raw, import_id) + def process_and_upload_inline_images(raw) inline_image_regex = /\[img\]([\s\S]*?)\[\/img\]/ + s = raw.dup s.gsub!(inline_image_regex) do |d| @@ -361,14 +545,65 @@ class ImportScripts::DiscuzX < ImportScripts::Base upload ? html_for_upload(upload, filename) : nil end + end + + def process_discuzx_post(raw, import_id) + # raw = process_and_upload_inline_images(raw) + s = raw.dup + # Strip the quote # [quote] quotation includes the topic which is the same as reply to in Discourse # We get the pid to find the post number the post reply to. So it can be stripped - s = s.gsub(/\[quote\][\s\S]*?\[\/quote\]/i, '').strip s = s.gsub(/\[b\]回复 \[url=forum.php\?mod=redirect&goto=findpost&pid=\d+&ptid=\d+\].* 的帖子\[\/url\]\[\/b\]/i, '').strip + s = s.gsub(/\[b\]回复 \[url=https?:\/\/#{ORIGINAL_SITE_PREFIX}\/redirect.php\?goto=findpost&pid=\d+&ptid=\d+\].*?\[\/url\].*?\[\/b\]/i, '').strip - # Convert image bbcode + s.gsub!(/\[quote\](.*)?\[\/quote\]/im) do |matched| + content = $1 + post_import_id = find_post_id_by_quote_number(content) + if post_import_id + post_id = post_id_from_imported_post_id(post_import_id.to_i) + if (post = Post.find_by(id: post_id)) + "[quote=\"#{post.user.username}\", post: #{post.post_number}, topic: #{post.topic_id}]\n#{content}\n[/quote]" + else + puts "post #{import_id} quote to not exists post #{post_import_id}, skip reply" + matched[0] + end + else + matched[0] + end + end + + s.gsub!(/\[size=2\]\[color=#999999\].*? 发表于 [\d\-\: ]*\[\/color\] \[url=forum.php\?mod=redirect&goto=findpost&pid=\d+&ptid=\d+\].*?\[\/url\]\[\/size\]/i, '') + s.gsub!(/\[size=2\]\[color=#999999\].*? 发表于 [\d\-\: ]*\[\/color\] \[url=https?:\/\/#{ORIGINAL_SITE_PREFIX}\/redirect.php\?goto=findpost&pid=\d+&ptid=\d+\].*?\[\/url\]\[\/size\]/i, '') + + # convert quote + s.gsub!(/\[quote\](.*?)\[\/quote\]/m) { "\n" + ($1.strip).gsub(/^/, '> ') + "\n" } + + # truncate line space, preventing line starting with many blanks to be parsed as code blocks + s.gsub!(/^ {4,}/, ' ') + + # TODO: Much better to use bbcode-to-md gem + # Convert image bbcode with width and height + s.gsub!(/\[img[^\]]*\]https?:\/\/#{ORIGINAL_SITE_PREFIX}\/(.*)\[\/img\]/i, '[x-attach]\1[/x-attach]') # dont convert attachment + s.gsub!(/]*src="https?:\/\/#{ORIGINAL_SITE_PREFIX}\/(.*)".*?>/i, '[x-attach]\1[/x-attach]') # dont convert attachment + s.gsub!(/\[img[^\]]*\]https?:\/\/www\.touhou\.cc\/blog\/(.*)\[\/img\]/i, '[x-attach]../blog/\1[/x-attach]') # 私货 + s.gsub!(/\[img[^\]]*\]https?:\/\/www\.touhou\.cc\/ucenter\/avatar.php\?uid=(\d+)[^\]]*\[\/img\]/i) { "[x-attach]#{discuzx_avatar_fullpath($1,false)[0]}[/x-attach]" } # 私货 s.gsub!(/\[img=(\d+),(\d+)\]([^\]]*)\[\/img\]/i, '') + s.gsub!(/\[img\]([^\]]*)\[\/img\]/i, '') + + s.gsub!(/\[qq\]([^\]]*)\[\/qq\]/i, 'QQ 交谈') + + s.gsub!(/\[email\]([^\]]*)\[\/email\]/i, '[url=mailto:\1]\1[/url]') # bbcode-to-md can convert it + s.gsub!(/\[s\]([^\]]*)\[\/s\]/i, '\1') + s.gsub!(/\[sup\]([^\]]*)\[\/sup\]/i, '\1') + s.gsub!(/\[sub\]([^\]]*)\[\/sub\]/i, '\1') + s.gsub!(/\[hr\]/i, "\n---\n") + + # remove the media tag + s.gsub!(/\[\/?media[^\]]*\]/i, "\n") + s.gsub!(/\[\/?flash[^\]]*\]/i, "\n") + s.gsub!(/\[\/?audio[^\]]*\]/i, "\n") + s.gsub!(/\[\/?video[^\]]*\]/i, "\n") # Remove the font, p and backcolor tag # Discourse doesn't support the font tag @@ -390,11 +625,14 @@ class ImportScripts::DiscuzX < ImportScripts::Base # Remove the hide tag s.gsub!(/\[\/?hide\]/i, '') + s.gsub!(/\[\/?free[^\]]*\]/i, "\n") # Remove the align tag # still don't know what it is - s.gsub!(/\[align=[^\]]*?\]/i, '') + s.gsub!(/\[align=[^\]]*?\]/i, "\n") s.gsub!(/\[\/align\]/i, "\n") + s.gsub!(/\[float=[^\]]*?\]/i, "\n") + s.gsub!(/\[\/float\]/i, "\n") # Convert code s.gsub!(/\[\/?code\]/i, "\n```\n") @@ -424,39 +662,65 @@ class ImportScripts::DiscuzX < ImportScripts::Base # [url][b]text[/b][/url] to **[url]text[/url]** s.gsub!(/(\[url=[^\[\]]*?\])\[b\](\S*)\[\/b\](\[\/url\])/, '**\1\2\3**') - s.gsub!(internal_url_regexp) do |discuzx_link| - replace_internal_link(discuzx_link, $1) + @internal_url_regexps.each do |internal_url_regexp| + s.gsub!(internal_url_regexp) do |discuzx_link| + replace_internal_link(discuzx_link, ($~[:tid].to_i rescue nil), ($~[:pid].to_i rescue nil), ($~[:fid].to_i rescue nil), ($~[:action] rescue nil)) + end end # @someone without the url s.gsub!(/@\[url=[^\[\]]*?\](\S*)\[\/url\]/i, '@\1') + s.scan(/http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/[^\[\]\s]*/) {|link|puts "WARNING: post #{import_id} can't replace internal url #{link}"} + s.strip end - def replace_internal_link(discuzx_link, import_topic_id) - results = mysql_query("SELECT pid - FROM #{table_name 'forum_post'} - WHERE tid = #{import_topic_id} - ORDER BY pid ASC - LIMIT 1") - - return discuzx_link unless results.size > 0 - - linked_topic_id = results.first['pid'] - lookup = topic_lookup_from_imported_post_id(linked_topic_id) - - return discuzx_link unless lookup - - if (t = Topic.find_by(id: lookup[:topic_id])) - "#{NEW_SITE_PREFIX}/t/#{t.slug}/#{t.id}" - else - discuzx_link + def replace_internal_link(discuzx_link, import_topic_id, import_post_id, import_category_id, action) + if import_post_id + post_id = post_id_from_imported_post_id import_post_id + if post_id + post = Post.find post_id + return post.full_url if post + end end - end - def internal_url_regexp - @internal_url_regexp ||= Regexp.new("http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}/forum\\.php\\?mod=viewthread&tid=(\\d+)(?:[^\\]\\[]*)") + if import_topic_id + + results = mysql_query("SELECT pid + FROM #{table_name 'forum_post'} + WHERE tid = #{import_topic_id} AND first + LIMIT 1") + + return discuzx_link unless results.size > 0 + + linked_post_id = results.first['pid'] + lookup = topic_lookup_from_imported_post_id(linked_post_id) + + if lookup + return "#{NEW_SITE_PREFIX}#{lookup[:url]}" + else + return discuzx_link + end + + end + + if import_category_id + category_id = category_id_from_imported_category_id import_category_id + if category_id + category = Category.find category_id + return category.url if category + end + end + + case action + when 'index' + return "#{NEW_SITE_PREFIX}/" + when 'stat', 'stats', 'ranklist' + return "#{NEW_SITE_PREFIX}/users" + end + + discuzx_link end def pm_url_regexp @@ -470,8 +734,7 @@ class ImportScripts::DiscuzX < ImportScripts::Base SiteSetting.authorized_extensions = setting if setting != SiteSetting.authorized_extensions attachment_regex = /\[attach\](\d+)\[\/attach\]/ - - user = Discourse.system_user + attachment_link_regex = /\[x-attach\](.+)\[\/x-attach\]/ current_count = 0 total_count = mysql_query("SELECT count(*) count FROM #{table_name 'forum_post'};").first['count'] @@ -482,13 +745,20 @@ class ImportScripts::DiscuzX < ImportScripts::Base puts '', "Importing attachments...", '' Post.find_each do |post| + next unless post.custom_fields['import_id'] == post.custom_fields['import_id'].to_i.to_s + + user = post.user + current_count += 1 print_status current_count, total_count new_raw = post.raw.dup + + inline_attachments = [] + new_raw.gsub!(attachment_regex) do |s| - matches = attachment_regex.match(s) - attachment_id = matches[1] + attachment_id = $1.to_i + inline_attachments.push attachment_id upload, filename = find_upload(user, post, attachment_id) unless upload @@ -498,6 +768,41 @@ class ImportScripts::DiscuzX < ImportScripts::Base html_for_upload(upload, filename) end + new_raw.gsub!(attachment_link_regex) do |s| + attachment_file = $1 + + filename = File.basename(attachment_file) + upload = create_upload(user.id, File.join(DISCUZX_BASE_DIR, attachment_file), filename) + unless upload + fail_count += 1 + next + end + + html_for_upload(upload, filename) + end + + sql = "SELECT aid + FROM #{table_name 'forum_attachment'} + WHERE pid = #{post.custom_fields['import_id']}" + if !inline_attachments.empty? + sql << " AND aid NOT IN (#{inline_attachments.join(',')})" + end + + results = mysql_query(sql) + + results.each do |attachment| + attachment_id = attachment['aid'] + upload, filename = find_upload(user, post, attachment_id) + unless upload + fail_count += 1 + next + end + html = html_for_upload(upload, filename) + unless new_raw.include? html + new_raw << "\n" + new_raw << html + end + end if new_raw != post.raw PostRevisor.new(post).revise!(post.user, { raw: new_raw }, { bypass_bump: true, edit_reason: '从 Discuz 中导入附件' }) @@ -513,7 +818,7 @@ class ImportScripts::DiscuzX < ImportScripts::Base end # Create the full path to the discuz avatar specified from user id - def discuzx_avatar_fullpath(user_id) + def discuzx_avatar_fullpath(user_id, absolute=true) padded_id = user_id.to_s.rjust(9, '0') part_1 = padded_id[0..2] @@ -522,16 +827,23 @@ class ImportScripts::DiscuzX < ImportScripts::Base part_4 = padded_id[-2..-1] file_name = "#{part_4}_avatar_big.jpg" - return File.join(DISCUZX_BASE_DIR, AVATAR_DIR, part_1, part_2, part_3, file_name), file_name + if absolute + return File.join(DISCUZX_BASE_DIR, AVATAR_DIR, part_1, part_2, part_3, file_name), file_name + else + return File.join(AVATAR_DIR, part_1, part_2, part_3, file_name), file_name + end end # post id is in the quote block def find_post_id_by_quote_number(raw) - s = raw.dup - quote_reply = s.match(/\[quote\][\S\s]*pid=(\d+)[\S\s]*\[\/quote\]/) - reply = s.match(/url=forum.php\?mod=redirect&goto=findpost&pid=(\d+)&ptid=\d+/) - - quote_reply ? quote_reply[1] : (reply ? reply[1] : nil) + case raw + when /\[url=forum.php\?mod=redirect&goto=findpost&pid=(\d+)&ptid=\d+\]/ #standard + $1 + when /\[url=https?:\/\/#{ORIGINAL_SITE_PREFIX}\/redirect.php\?goto=findpost&pid=(\d+)&ptid=\d+\]/ # old discuz 7 format + $1 + when /\[quote\][\S\s]*pid=(\d+)[\S\s]*\[\/quote\]/ # quote + $1 + end end # for some reason, discuz inlined some png file @@ -632,6 +944,10 @@ class ImportScripts::DiscuzX < ImportScripts::Base return nil end + def first_exists(*items) + items.find{|item|!item.blank?} || '' + end + def mysql_query(sql) @client.query(sql, cache_rows: false) end From 8ced8350bacb859316f379e55338d3f6e19cd0c9 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 5 Feb 2016 21:42:48 +0100 Subject: [PATCH 023/140] Upgrade moment.js to version 2.11.2 In order to make future upgrades easier we don't rename the locale files anymore. --- lib/javascripts/moment.js | 5362 ++++++++++++--------- lib/javascripts/moment_locale/af.js | 82 +- lib/javascripts/moment_locale/ar-ma.js | 81 +- lib/javascripts/moment_locale/ar-sa.js | 89 +- lib/javascripts/moment_locale/ar-tn.js | 57 + lib/javascripts/moment_locale/ar.js | 133 +- lib/javascripts/moment_locale/az.js | 135 +- lib/javascripts/moment_locale/be.js | 114 +- lib/javascripts/moment_locale/bg.js | 78 +- lib/javascripts/moment_locale/bn.js | 91 +- lib/javascripts/moment_locale/bo.js | 91 +- lib/javascripts/moment_locale/br.js | 80 +- lib/javascripts/moment_locale/bs.js | 69 +- lib/javascripts/moment_locale/ca.js | 89 +- lib/javascripts/moment_locale/cs.js | 76 +- lib/javascripts/moment_locale/cv.js | 86 +- lib/javascripts/moment_locale/cy.js | 80 +- lib/javascripts/moment_locale/da.js | 78 +- lib/javascripts/moment_locale/de-at.js | 79 +- lib/javascripts/moment_locale/de.js | 77 +- lib/javascripts/moment_locale/dv.js | 99 + lib/javascripts/moment_locale/el.js | 98 +- lib/javascripts/moment_locale/en-au.js | 76 +- lib/javascripts/moment_locale/en-ca.js | 78 +- lib/javascripts/moment_locale/en-gb.js | 78 +- lib/javascripts/moment_locale/en-ie.js | 67 + lib/javascripts/moment_locale/en-nz.js | 66 + lib/javascripts/moment_locale/eo.js | 88 +- lib/javascripts/moment_locale/es.js | 80 +- lib/javascripts/moment_locale/et.js | 58 +- lib/javascripts/moment_locale/eu.js | 86 +- lib/javascripts/moment_locale/fa.js | 44 +- lib/javascripts/moment_locale/fi.js | 72 +- lib/javascripts/moment_locale/fo.js | 78 +- lib/javascripts/moment_locale/fr-ca.js | 82 +- lib/javascripts/moment_locale/fr-ch.js | 62 + lib/javascripts/moment_locale/fr.js | 80 +- lib/javascripts/moment_locale/fy.js | 71 + lib/javascripts/moment_locale/gd.js | 76 + lib/javascripts/moment_locale/gl.js | 82 +- lib/javascripts/moment_locale/he.js | 99 +- lib/javascripts/moment_locale/hi.js | 102 +- lib/javascripts/moment_locale/hr.js | 70 +- lib/javascripts/moment_locale/hu.js | 64 +- lib/javascripts/moment_locale/hy-am.js | 119 +- lib/javascripts/moment_locale/id.js | 92 +- lib/javascripts/moment_locale/is.js | 61 +- lib/javascripts/moment_locale/it.js | 89 +- lib/javascripts/moment_locale/ja.js | 85 +- lib/javascripts/moment_locale/jv.js | 83 + lib/javascripts/moment_locale/ka.js | 120 +- lib/javascripts/moment_locale/kk.js | 87 + lib/javascripts/moment_locale/km.js | 79 +- lib/javascripts/moment_locale/ko.js | 99 +- lib/javascripts/moment_locale/lb.js | 87 +- lib/javascripts/moment_locale/lo.js | 69 + lib/javascripts/moment_locale/lt.js | 127 +- lib/javascripts/moment_locale/lv.js | 101 +- lib/javascripts/moment_locale/me.js | 109 + lib/javascripts/moment_locale/mk.js | 84 +- lib/javascripts/moment_locale/ml.js | 91 +- lib/javascripts/moment_locale/mr.js | 142 +- lib/javascripts/moment_locale/ms-my.js | 90 +- lib/javascripts/moment_locale/ms.js | 82 + lib/javascripts/moment_locale/my.js | 79 +- lib/javascripts/moment_locale/nb.js | 80 +- lib/javascripts/moment_locale/nb_NO.js | 46 - lib/javascripts/moment_locale/ne.js | 114 +- lib/javascripts/moment_locale/nl.js | 80 +- lib/javascripts/moment_locale/nn.js | 78 +- lib/javascripts/moment_locale/pl.js | 75 +- lib/javascripts/moment_locale/pt-br.js | 78 +- lib/javascripts/moment_locale/pt.js | 78 +- lib/javascripts/moment_locale/pt_BR.js | 46 - lib/javascripts/moment_locale/ro.js | 72 +- lib/javascripts/moment_locale/ru.js | 184 +- lib/javascripts/moment_locale/se.js | 61 + lib/javascripts/moment_locale/si.js | 66 + lib/javascripts/moment_locale/sk.js | 72 +- lib/javascripts/moment_locale/sl.js | 140 +- lib/javascripts/moment_locale/sq.js | 86 +- lib/javascripts/moment_locale/sr-cyrl.js | 55 +- lib/javascripts/moment_locale/sr.js | 55 +- lib/javascripts/moment_locale/sv.js | 82 +- lib/javascripts/moment_locale/sw.js | 58 + lib/javascripts/moment_locale/ta.js | 177 +- lib/javascripts/moment_locale/te.js | 88 + lib/javascripts/moment_locale/th.js | 85 +- lib/javascripts/moment_locale/tl-ph.js | 80 +- lib/javascripts/moment_locale/tlh.js | 119 + lib/javascripts/moment_locale/tr.js | 124 +- lib/javascripts/moment_locale/tzl.js | 87 + lib/javascripts/moment_locale/tzm-latn.js | 79 +- lib/javascripts/moment_locale/tzm.js | 79 +- lib/javascripts/moment_locale/uk.js | 107 +- lib/javascripts/moment_locale/uz.js | 77 +- lib/javascripts/moment_locale/vi.js | 88 +- lib/javascripts/moment_locale/zh-cn.js | 143 +- lib/javascripts/moment_locale/zh-tw.js | 127 +- lib/javascripts/moment_locale/zh_CN.js | 73 - lib/javascripts/moment_locale/zh_TW.js | 73 - 101 files changed, 8247 insertions(+), 5823 deletions(-) create mode 100644 lib/javascripts/moment_locale/ar-tn.js create mode 100644 lib/javascripts/moment_locale/dv.js create mode 100644 lib/javascripts/moment_locale/en-ie.js create mode 100644 lib/javascripts/moment_locale/en-nz.js create mode 100644 lib/javascripts/moment_locale/fr-ch.js create mode 100644 lib/javascripts/moment_locale/fy.js create mode 100644 lib/javascripts/moment_locale/gd.js create mode 100644 lib/javascripts/moment_locale/jv.js create mode 100644 lib/javascripts/moment_locale/kk.js create mode 100644 lib/javascripts/moment_locale/lo.js create mode 100644 lib/javascripts/moment_locale/me.js create mode 100644 lib/javascripts/moment_locale/ms.js delete mode 100644 lib/javascripts/moment_locale/nb_NO.js delete mode 100644 lib/javascripts/moment_locale/pt_BR.js create mode 100644 lib/javascripts/moment_locale/se.js create mode 100644 lib/javascripts/moment_locale/si.js create mode 100644 lib/javascripts/moment_locale/sw.js create mode 100644 lib/javascripts/moment_locale/te.js create mode 100644 lib/javascripts/moment_locale/tlh.js create mode 100644 lib/javascripts/moment_locale/tzl.js delete mode 100644 lib/javascripts/moment_locale/zh_CN.js delete mode 100644 lib/javascripts/moment_locale/zh_TW.js diff --git a/lib/javascripts/moment.js b/lib/javascripts/moment.js index 03a2460105..7299fa45c2 100644 --- a/lib/javascripts/moment.js +++ b/lib/javascripts/moment.js @@ -1,468 +1,165 @@ //! moment.js -//! version : 2.8.1 +//! version : 2.11.2 //! authors : Tim Wood, Iskren Chernev, Moment.js contributors //! license : MIT //! momentjs.com -(function (undefined) { - /************************************ - Constants - ************************************/ +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + global.moment = factory() +}(this, function () { 'use strict'; - var moment, - VERSION = '2.8.1', - // the global-scope this is NOT the global object in Node.js - globalScope = typeof global !== 'undefined' ? global : this, - oldGlobalMoment, - round = Math.round, - i, + var hookCallback; - YEAR = 0, - MONTH = 1, - DATE = 2, - HOUR = 3, - MINUTE = 4, - SECOND = 5, - MILLISECOND = 6, + function utils_hooks__hooks () { + return hookCallback.apply(null, arguments); + } - // internal storage for locale config files - locales = {}, + // This is done to register the method called with moment() + // without creating circular dependencies. + function setHookCallback (callback) { + hookCallback = callback; + } - // extra moment internal properties (plugins register props here) - momentProperties = [], + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } - // check for nodeJS - hasModule = (typeof module !== 'undefined' && module.exports), + function isDate(input) { + return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; + } - // ASP.NET json date format regex - aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, - aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, - - // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html - // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere - isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/, - - // format tokens - formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g, - localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g, - - // parsing token regexes - parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 - parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 - parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999 - parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 - parseTokenDigits = /\d+/, // nonzero number of digits - parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic. - parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z - parseTokenT = /T/i, // T (ISO separator) - parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 - parseTokenOrdinal = /\d{1,2}/, - - //strict parsing regexes - parseTokenOneDigit = /\d/, // 0 - 9 - parseTokenTwoDigits = /\d\d/, // 00 - 99 - parseTokenThreeDigits = /\d{3}/, // 000 - 999 - parseTokenFourDigits = /\d{4}/, // 0000 - 9999 - parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999 - parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf - - // iso 8601 regex - // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) - isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/, - - isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', - - isoDates = [ - ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], - ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], - ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], - ['GGGG-[W]WW', /\d{4}-W\d{2}/], - ['YYYY-DDD', /\d{4}-\d{3}/] - ], - - // iso time formats and regexes - isoTimes = [ - ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], - ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], - ['HH:mm', /(T| )\d\d:\d\d/], - ['HH', /(T| )\d\d/] - ], - - // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] - parseTimezoneChunker = /([\+\-]|\d\d)/gi, - - // getter and setter names - proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), - unitMillisecondFactors = { - 'Milliseconds' : 1, - 'Seconds' : 1e3, - 'Minutes' : 6e4, - 'Hours' : 36e5, - 'Days' : 864e5, - 'Months' : 2592e6, - 'Years' : 31536e6 - }, - - unitAliases = { - ms : 'millisecond', - s : 'second', - m : 'minute', - h : 'hour', - d : 'day', - D : 'date', - w : 'week', - W : 'isoWeek', - M : 'month', - Q : 'quarter', - y : 'year', - DDD : 'dayOfYear', - e : 'weekday', - E : 'isoWeekday', - gg: 'weekYear', - GG: 'isoWeekYear' - }, - - camelFunctions = { - dayofyear : 'dayOfYear', - isoweekday : 'isoWeekday', - isoweek : 'isoWeek', - weekyear : 'weekYear', - isoweekyear : 'isoWeekYear' - }, - - // format function strings - formatFunctions = {}, - - // default relative time thresholds - relativeTimeThresholds = { - s: 45, // seconds to minute - m: 45, // minutes to hour - h: 22, // hours to day - d: 26, // days to month - M: 11 // months to year - }, - - // tokens to ordinalize and pad - ordinalizeTokens = 'DDD w W M D d'.split(' '), - paddedTokens = 'M D H h m s w W'.split(' '), - - formatTokenFunctions = { - M : function () { - return this.month() + 1; - }, - MMM : function (format) { - return this.localeData().monthsShort(this, format); - }, - MMMM : function (format) { - return this.localeData().months(this, format); - }, - D : function () { - return this.date(); - }, - DDD : function () { - return this.dayOfYear(); - }, - d : function () { - return this.day(); - }, - dd : function (format) { - return this.localeData().weekdaysMin(this, format); - }, - ddd : function (format) { - return this.localeData().weekdaysShort(this, format); - }, - dddd : function (format) { - return this.localeData().weekdays(this, format); - }, - w : function () { - return this.week(); - }, - W : function () { - return this.isoWeek(); - }, - YY : function () { - return leftZeroFill(this.year() % 100, 2); - }, - YYYY : function () { - return leftZeroFill(this.year(), 4); - }, - YYYYY : function () { - return leftZeroFill(this.year(), 5); - }, - YYYYYY : function () { - var y = this.year(), sign = y >= 0 ? '+' : '-'; - return sign + leftZeroFill(Math.abs(y), 6); - }, - gg : function () { - return leftZeroFill(this.weekYear() % 100, 2); - }, - gggg : function () { - return leftZeroFill(this.weekYear(), 4); - }, - ggggg : function () { - return leftZeroFill(this.weekYear(), 5); - }, - GG : function () { - return leftZeroFill(this.isoWeekYear() % 100, 2); - }, - GGGG : function () { - return leftZeroFill(this.isoWeekYear(), 4); - }, - GGGGG : function () { - return leftZeroFill(this.isoWeekYear(), 5); - }, - e : function () { - return this.weekday(); - }, - E : function () { - return this.isoWeekday(); - }, - a : function () { - return this.localeData().meridiem(this.hours(), this.minutes(), true); - }, - A : function () { - return this.localeData().meridiem(this.hours(), this.minutes(), false); - }, - H : function () { - return this.hours(); - }, - h : function () { - return this.hours() % 12 || 12; - }, - m : function () { - return this.minutes(); - }, - s : function () { - return this.seconds(); - }, - S : function () { - return toInt(this.milliseconds() / 100); - }, - SS : function () { - return leftZeroFill(toInt(this.milliseconds() / 10), 2); - }, - SSS : function () { - return leftZeroFill(this.milliseconds(), 3); - }, - SSSS : function () { - return leftZeroFill(this.milliseconds(), 3); - }, - Z : function () { - var a = -this.zone(), - b = '+'; - if (a < 0) { - a = -a; - b = '-'; - } - return b + leftZeroFill(toInt(a / 60), 2) + ':' + leftZeroFill(toInt(a) % 60, 2); - }, - ZZ : function () { - var a = -this.zone(), - b = '+'; - if (a < 0) { - a = -a; - b = '-'; - } - return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2); - }, - z : function () { - return this.zoneAbbr(); - }, - zz : function () { - return this.zoneName(); - }, - X : function () { - return this.unix(); - }, - Q : function () { - return this.quarter(); - } - }, - - deprecations = {}, - - lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin']; - - // Pick the first defined of two or three arguments. dfl comes from - // default. - function dfl(a, b, c) { - switch (arguments.length) { - case 2: return a != null ? a : b; - case 3: return a != null ? a : b != null ? b : c; - default: throw new Error('Implement me'); + function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); } + return res; } - function defaultParsingFlags() { - // We need to deep clone this object, and es5 standard is not very - // helpful. - return { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso: false - }; + function hasOwnProp(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); } - function printMsg(msg) { - if (moment.suppressDeprecationWarnings === false && - typeof console !== 'undefined' && console.warn) { - console.warn("Deprecation warning: " + msg); - } - } - - function deprecate(msg, fn) { - var firstTime = true; - return extend(function () { - if (firstTime) { - printMsg(msg); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); - } - - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - printMsg(msg); - deprecations[name] = true; - } - } - - function padToken(func, count) { - return function (a) { - return leftZeroFill(func.call(this, a), count); - }; - } - function ordinalizeToken(func, period) { - return function (a) { - return this.localeData().ordinal(func.call(this, a), period); - }; - } - - while (ordinalizeTokens.length) { - i = ordinalizeTokens.pop(); - formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); - } - while (paddedTokens.length) { - i = paddedTokens.pop(); - formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); - } - formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); - - - /************************************ - Constructors - ************************************/ - - function Locale() { - } - - // Moment prototype object - function Moment(config, skipOverflow) { - if (skipOverflow !== false) { - checkOverflow(config); - } - copyConfig(this, config); - this._d = new Date(+config._d); - } - - // Duration Constructor - function Duration(duration) { - var normalizedInput = normalizeObjectUnits(duration), - years = normalizedInput.year || 0, - quarters = normalizedInput.quarter || 0, - months = normalizedInput.month || 0, - weeks = normalizedInput.week || 0, - days = normalizedInput.day || 0, - hours = normalizedInput.hour || 0, - minutes = normalizedInput.minute || 0, - seconds = normalizedInput.second || 0, - milliseconds = normalizedInput.millisecond || 0; - - // representation for dateAddRemove - this._milliseconds = +milliseconds + - seconds * 1e3 + // 1000 - minutes * 6e4 + // 1000 * 60 - hours * 36e5; // 1000 * 60 * 60 - // Because of dateAddRemove treats 24 hours as different from a - // day when working around DST, we need to store them separately - this._days = +days + - weeks * 7; - // It is impossible translate months into days without knowing - // which months you are are talking about, so we have to store - // it separately. - this._months = +months + - quarters * 3 + - years * 12; - - this._data = {}; - - this._locale = moment.localeData(); - - this._bubble(); - } - - /************************************ - Helpers - ************************************/ - - function extend(a, b) { for (var i in b) { - if (b.hasOwnProperty(i)) { + if (hasOwnProp(b, i)) { a[i] = b[i]; } } - if (b.hasOwnProperty('toString')) { + if (hasOwnProp(b, 'toString')) { a.toString = b.toString; } - if (b.hasOwnProperty('valueOf')) { + if (hasOwnProp(b, 'valueOf')) { a.valueOf = b.valueOf; } return a; } + function create_utc__createUTC (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, true).utc(); + } + + function defaultParsingFlags() { + // We need to deep clone this object. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso : false + }; + } + + function getParsingFlags(m) { + if (m._pf == null) { + m._pf = defaultParsingFlags(); + } + return m._pf; + } + + function valid__isValid(m) { + if (m._isValid == null) { + var flags = getParsingFlags(m); + m._isValid = !isNaN(m._d.getTime()) && + flags.overflow < 0 && + !flags.empty && + !flags.invalidMonth && + !flags.invalidWeekday && + !flags.nullInput && + !flags.invalidFormat && + !flags.userInvalidated; + + if (m._strict) { + m._isValid = m._isValid && + flags.charsLeftOver === 0 && + flags.unusedTokens.length === 0 && + flags.bigHour === undefined; + } + } + return m._isValid; + } + + function valid__createInvalid (flags) { + var m = create_utc__createUTC(NaN); + if (flags != null) { + extend(getParsingFlags(m), flags); + } + else { + getParsingFlags(m).userInvalidated = true; + } + + return m; + } + + function isUndefined(input) { + return input === void 0; + } + + // Plugins that add properties should also add the key here (null value), + // so we can properly clone ourselves. + var momentProperties = utils_hooks__hooks.momentProperties = []; + function copyConfig(to, from) { var i, prop, val; - if (typeof from._isAMomentObject !== 'undefined') { + if (!isUndefined(from._isAMomentObject)) { to._isAMomentObject = from._isAMomentObject; } - if (typeof from._i !== 'undefined') { + if (!isUndefined(from._i)) { to._i = from._i; } - if (typeof from._f !== 'undefined') { + if (!isUndefined(from._f)) { to._f = from._f; } - if (typeof from._l !== 'undefined') { + if (!isUndefined(from._l)) { to._l = from._l; } - if (typeof from._strict !== 'undefined') { + if (!isUndefined(from._strict)) { to._strict = from._strict; } - if (typeof from._tzm !== 'undefined') { + if (!isUndefined(from._tzm)) { to._tzm = from._tzm; } - if (typeof from._isUTC !== 'undefined') { + if (!isUndefined(from._isUTC)) { to._isUTC = from._isUTC; } - if (typeof from._offset !== 'undefined') { + if (!isUndefined(from._offset)) { to._offset = from._offset; } - if (typeof from._pf !== 'undefined') { - to._pf = from._pf; + if (!isUndefined(from._pf)) { + to._pf = getParsingFlags(from); } - if (typeof from._locale !== 'undefined') { + if (!isUndefined(from._locale)) { to._locale = from._locale; } @@ -470,7 +167,7 @@ for (i in momentProperties) { prop = momentProperties[i]; val = from[prop]; - if (typeof val !== 'undefined') { + if (!isUndefined(val)) { to[prop] = val; } } @@ -479,7 +176,26 @@ return to; } - function absRound(number) { + var updateInProgress = false; + + // Moment prototype object + function Moment(config) { + copyConfig(this, config); + this._d = new Date(config._d != null ? config._d.getTime() : NaN); + // Prevent infinite loop in case updateOffset creates new moment + // objects. + if (updateInProgress === false) { + updateInProgress = true; + utils_hooks__hooks.updateOffset(this); + updateInProgress = false; + } + } + + function isMoment (obj) { + return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); + } + + function absFloor (number) { if (number < 0) { return Math.ceil(number); } else { @@ -487,91 +203,15 @@ } } - // left zero fill a number - // see http://jsperf.com/left-zero-filling for performance comparison - function leftZeroFill(number, targetLength, forceSign) { - var output = '' + Math.abs(number), - sign = number >= 0; + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; - while (output.length < targetLength) { - output = '0' + output; - } - return (sign ? (forceSign ? '+' : '') : '-') + output; - } - - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; - - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + value = absFloor(coercedNumber); } - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - - return res; - } - - function momentsDifference(base, other) { - var res; - other = makeAs(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); - } else { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; - } - - return res; - } - - // TODO: remove 'name' arg after deprecation is removed - function createAdder(direction, name) { - return function (val, period) { - var dur, tmp; - //invert the arguments, but complain about it - if (period !== null && !isNaN(+period)) { - deprecateSimple(name, "moment()." + name + "(period, number) is deprecated. Please use moment()." + name + "(number, period)."); - tmp = val; val = period; period = tmp; - } - - val = typeof val === 'string' ? +val : val; - dur = moment.duration(val, period); - addOrSubtractDurationFromMoment(this, dur, direction); - return this; - }; - } - - function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; - - if (milliseconds) { - mom._d.setTime(+mom._d + milliseconds * isAdding); - } - if (days) { - rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding); - } - if (months) { - rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding); - } - if (updateOffset) { - moment.updateOffset(mom, days || months); - } - } - - // check if is an array - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; - } - - function isDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || - input instanceof Date; + return value; } // compare two arrays, return the number of differences @@ -589,142 +229,12 @@ return diffs + lengthDiff; } - function normalizeUnits(units) { - if (units) { - var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); - units = unitAliases[units] || camelFunctions[lowered] || lowered; - } - return units; + function Locale() { } - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; - - for (prop in inputObject) { - if (inputObject.hasOwnProperty(prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } - } - - return normalizedInput; - } - - function makeList(field) { - var count, setter; - - if (field.indexOf('week') === 0) { - count = 7; - setter = 'day'; - } - else if (field.indexOf('month') === 0) { - count = 12; - setter = 'month'; - } - else { - return; - } - - moment[field] = function (format, index) { - var i, getter, - method = moment._locale[field], - results = []; - - if (typeof format === 'number') { - index = format; - format = undefined; - } - - getter = function (i) { - var m = moment().utc().set(setter, i); - return method.call(moment._locale, m, format || ''); - }; - - if (index != null) { - return getter(index); - } - else { - for (i = 0; i < count; i++) { - results.push(getter(i)); - } - return results; - } - }; - } - - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; - - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } - - return value; - } - - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } - - function weeksInYear(year, dow, doy) { - return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; - } - - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; - } - - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; - } - - function checkOverflow(m) { - var overflow; - if (m._a && m._pf.overflow === -2) { - overflow = - m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH : - m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE : - m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR : - m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE : - m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND : - m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND : - -1; - - if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } - - m._pf.overflow = overflow; - } - } - - function isValid(m) { - if (m._isValid == null) { - m._isValid = !isNaN(m._d.getTime()) && - m._pf.overflow < 0 && - !m._pf.empty && - !m._pf.invalidMonth && - !m._pf.nullInput && - !m._pf.invalidFormat && - !m._pf.userInvalidated; - - if (m._strict) { - m._isValid = m._isValid && - m._pf.charsLeftOver === 0 && - m._pf.unusedTokens.length === 0; - } - } - return m._isValid; - } + // internal storage for locale config files + var locales = {}; + var globalLocale; function normalizeLocale(key) { return key ? key.toLowerCase().replace('_', '-') : key; @@ -759,215 +269,196 @@ function loadLocale(name) { var oldLocale = null; - if (!locales[name] && hasModule) { + // TODO: Find a better way to register and load all the locales in Node + if (!locales[name] && (typeof module !== 'undefined') && + module && module.exports) { try { - oldLocale = moment.locale(); + oldLocale = globalLocale._abbr; require('./locale/' + name); - // because defineLocale currently also sets the global locale, we want to undo that for lazy loaded locales - moment.locale(oldLocale); + // because defineLocale currently also sets the global locale, we + // want to undo that for lazy loaded locales + locale_locales__getSetGlobalLocale(oldLocale); } catch (e) { } } return locales[name]; } - // Return a moment from input, that is local/utc/zone equivalent to model. - function makeAs(input, model) { - return model._isUTC ? moment(input).zone(model._offset || 0) : - moment(input).local(); + // This function will load locale and then set the global locale. If + // no arguments are passed in, it will simply return the current global + // locale key. + function locale_locales__getSetGlobalLocale (key, values) { + var data; + if (key) { + if (isUndefined(values)) { + data = locale_locales__getLocale(key); + } + else { + data = defineLocale(key, values); + } + + if (data) { + // moment.duration._locale = moment._locale = data; + globalLocale = data; + } + } + + return globalLocale._abbr; } - /************************************ - Locale - ************************************/ + function defineLocale (name, values) { + if (values !== null) { + values.abbr = name; + locales[name] = locales[name] || new Locale(); + locales[name].set(values); + // backwards compat for now: also set the locale + locale_locales__getSetGlobalLocale(name); - extend(Locale.prototype, { - - set : function (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (typeof prop === 'function') { - this[i] = prop; - } else { - this['_' + i] = prop; - } - } - }, - - _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), - months : function (m) { - return this._months[m.month()]; - }, - - _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), - monthsShort : function (m) { - return this._monthsShort[m.month()]; - }, - - monthsParse : function (monthName) { - var i, mom, regex; - - if (!this._monthsParse) { - this._monthsParse = []; - } - - for (i = 0; i < 12; i++) { - // make the regex if we don't have it already - if (!this._monthsParse[i]) { - mom = moment.utc([2000, i]); - regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); - this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._monthsParse[i].test(monthName)) { - return i; - } - } - }, - - _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), - weekdays : function (m) { - return this._weekdays[m.day()]; - }, - - _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), - weekdaysShort : function (m) { - return this._weekdaysShort[m.day()]; - }, - - _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), - weekdaysMin : function (m) { - return this._weekdaysMin[m.day()]; - }, - - weekdaysParse : function (weekdayName) { - var i, mom, regex; - - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } - - for (i = 0; i < 7; i++) { - // make the regex if we don't have it already - if (!this._weekdaysParse[i]) { - mom = moment([2000, 1]).day(i); - regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); - this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._weekdaysParse[i].test(weekdayName)) { - return i; - } - } - }, - - _longDateFormat : { - LT : 'h:mm A', - L : 'MM/DD/YYYY', - LL : 'MMMM D, YYYY', - LLL : 'MMMM D, YYYY LT', - LLLL : 'dddd, MMMM D, YYYY LT' - }, - longDateFormat : function (key) { - var output = this._longDateFormat[key]; - if (!output && this._longDateFormat[key.toUpperCase()]) { - output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { - return val.slice(1); - }); - this._longDateFormat[key] = output; - } - return output; - }, - - isPM : function (input) { - // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays - // Using charAt should be more compatible. - return ((input + '').toLowerCase().charAt(0) === 'p'); - }, - - _meridiemParse : /[ap]\.?m?\.?/i, - meridiem : function (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } - }, - - _calendar : { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[Last] dddd [at] LT', - sameElse : 'L' - }, - calendar : function (key, mom) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.apply(mom) : output; - }, - - _relativeTime : { - future : 'in %s', - past : '%s ago', - s : 'a few seconds', - m : 'a minute', - mm : '%d minutes', - h : 'an hour', - hh : '%d hours', - d : 'a day', - dd : '%d days', - M : 'a month', - MM : '%d months', - y : 'a year', - yy : '%d years' - }, - - relativeTime : function (number, withoutSuffix, string, isFuture) { - var output = this._relativeTime[string]; - return (typeof output === 'function') ? - output(number, withoutSuffix, string, isFuture) : - output.replace(/%d/i, number); - }, - - pastFuture : function (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); - }, - - ordinal : function (number) { - return this._ordinal.replace('%d', number); - }, - _ordinal : '%d', - - preparse : function (string) { - return string; - }, - - postformat : function (string) { - return string; - }, - - week : function (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - }, - - _week : { - dow : 0, // Sunday is the first day of the week. - doy : 6 // The week that contains Jan 1st is the first week of the year. - }, - - _invalidDate: 'Invalid date', - invalidDate: function () { - return this._invalidDate; + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; } - }); + } - /************************************ - Formatting - ************************************/ + // returns locale data + function locale_locales__getLocale (key) { + var locale; + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } + + if (!key) { + return globalLocale; + } + + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } + + return chooseLocale(key); + } + + var aliases = {}; + + function addUnitAlias (unit, shorthand) { + var lowerCase = unit.toLowerCase(); + aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; + } + + function normalizeUnits(units) { + return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; + } + + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; + + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } + + return normalizedInput; + } + + function isFunction(input) { + return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]'; + } + + function makeGetSet (unit, keepTime) { + return function (value) { + if (value != null) { + get_set__set(this, unit, value); + utils_hooks__hooks.updateOffset(this, keepTime); + return this; + } else { + return get_set__get(this, unit); + } + }; + } + + function get_set__get (mom, unit) { + return mom.isValid() ? + mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() : NaN; + } + + function get_set__set (mom, unit, value) { + if (mom.isValid()) { + mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } + } + + // MOMENTS + + function getSet (units, value) { + var unit; + if (typeof units === 'object') { + for (unit in units) { + this.set(unit, units[unit]); + } + } else { + units = normalizeUnits(units); + if (isFunction(this[units])) { + return this[units](value); + } + } + return this; + } + + function zeroFill(number, targetLength, forceSign) { + var absNumber = '' + Math.abs(number), + zerosToFill = targetLength - absNumber.length, + sign = number >= 0; + return (sign ? (forceSign ? '+' : '') : '-') + + Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber; + } + + var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g; + + var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; + + var formatFunctions = {}; + + var formatTokenFunctions = {}; + + // token: 'M' + // padded: ['MM', 2] + // ordinal: 'Mo' + // callback: function () { this.month() + 1 } + function addFormatToken (token, padded, ordinal, callback) { + var func = callback; + if (typeof callback === 'string') { + func = function () { + return this[callback](); + }; + } + if (token) { + formatTokenFunctions[token] = func; + } + if (padded) { + formatTokenFunctions[padded[0]] = function () { + return zeroFill(func.apply(this, arguments), padded[1], padded[2]); + }; + } + if (ordinal) { + formatTokenFunctions[ordinal] = function () { + return this.localeData().ordinal(func.apply(this, arguments), token); + }; + } + } function removeFormattingTokens(input) { if (input.match(/\[[\s\S]/)) { @@ -1003,10 +494,7 @@ } format = expandFormat(format, m.localeData()); - - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } + formatFunctions[format] = formatFunctions[format] || makeFormatFunction(format); return formatFunctions[format](m); } @@ -1028,292 +516,642 @@ return format; } + var match1 = /\d/; // 0 - 9 + var match2 = /\d\d/; // 00 - 99 + var match3 = /\d{3}/; // 000 - 999 + var match4 = /\d{4}/; // 0000 - 9999 + var match6 = /[+-]?\d{6}/; // -999999 - 999999 + var match1to2 = /\d\d?/; // 0 - 99 + var match3to4 = /\d\d\d\d?/; // 999 - 9999 + var match5to6 = /\d\d\d\d\d\d?/; // 99999 - 999999 + var match1to3 = /\d{1,3}/; // 0 - 999 + var match1to4 = /\d{1,4}/; // 0 - 9999 + var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 - /************************************ - Parsing - ************************************/ + var matchUnsigned = /\d+/; // 0 - inf + var matchSigned = /[+-]?\d+/; // -inf - inf + + var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z + var matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi; // +00 -00 +00:00 -00:00 +0000 -0000 or Z + + var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 + + // any word (or two) characters or numbers including two/three word month in arabic. + // includes scottish gaelic two word and hyphenated months + var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; - // get the regex to find the next token - function getParseRegexForToken(token, config) { - var a, strict = config._strict; - switch (token) { - case 'Q': - return parseTokenOneDigit; - case 'DDDD': - return parseTokenThreeDigits; - case 'YYYY': - case 'GGGG': - case 'gggg': - return strict ? parseTokenFourDigits : parseTokenOneToFourDigits; - case 'Y': - case 'G': - case 'g': - return parseTokenSignedNumber; - case 'YYYYYY': - case 'YYYYY': - case 'GGGGG': - case 'ggggg': - return strict ? parseTokenSixDigits : parseTokenOneToSixDigits; - case 'S': - if (strict) { - return parseTokenOneDigit; + var regexes = {}; + + function addRegexToken (token, regex, strictRegex) { + regexes[token] = isFunction(regex) ? regex : function (isStrict, localeData) { + return (isStrict && strictRegex) ? strictRegex : regex; + }; + } + + function getParseRegexForToken (token, config) { + if (!hasOwnProp(regexes, token)) { + return new RegExp(unescapeFormat(token)); + } + + return regexes[token](config._strict, config._locale); + } + + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function unescapeFormat(s) { + return regexEscape(s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + })); + } + + function regexEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + + var tokens = {}; + + function addParseToken (token, callback) { + var i, func = callback; + if (typeof token === 'string') { + token = [token]; + } + if (typeof callback === 'number') { + func = function (input, array) { + array[callback] = toInt(input); + }; + } + for (i = 0; i < token.length; i++) { + tokens[token[i]] = func; + } + } + + function addWeekParseToken (token, callback) { + addParseToken(token, function (input, array, config, token) { + config._w = config._w || {}; + callback(input, config._w, config, token); + }); + } + + function addTimeToArrayFromToken(token, input, config) { + if (input != null && hasOwnProp(tokens, token)) { + tokens[token](input, config._a, config, token); + } + } + + var YEAR = 0; + var MONTH = 1; + var DATE = 2; + var HOUR = 3; + var MINUTE = 4; + var SECOND = 5; + var MILLISECOND = 6; + var WEEK = 7; + var WEEKDAY = 8; + + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } + + // FORMATTING + + addFormatToken('M', ['MM', 2], 'Mo', function () { + return this.month() + 1; + }); + + addFormatToken('MMM', 0, 0, function (format) { + return this.localeData().monthsShort(this, format); + }); + + addFormatToken('MMMM', 0, 0, function (format) { + return this.localeData().months(this, format); + }); + + // ALIASES + + addUnitAlias('month', 'M'); + + // PARSING + + addRegexToken('M', match1to2); + addRegexToken('MM', match1to2, match2); + addRegexToken('MMM', function (isStrict, locale) { + return locale.monthsShortRegex(isStrict); + }); + addRegexToken('MMMM', function (isStrict, locale) { + return locale.monthsRegex(isStrict); + }); + + addParseToken(['M', 'MM'], function (input, array) { + array[MONTH] = toInt(input) - 1; + }); + + addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { + var month = config._locale.monthsParse(input, token, config._strict); + // if we didn't find a month name, mark the date as invalid. + if (month != null) { + array[MONTH] = month; + } else { + getParsingFlags(config).invalidMonth = input; + } + }); + + // LOCALES + + var MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/; + var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); + function localeMonths (m, format) { + return isArray(this._months) ? this._months[m.month()] : + this._months[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()]; + } + + var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); + function localeMonthsShort (m, format) { + return isArray(this._monthsShort) ? this._monthsShort[m.month()] : + this._monthsShort[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()]; + } + + function localeMonthsParse (monthName, format, strict) { + var i, mom, regex; + + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } + + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = create_utc__createUTC([2000, i]); + if (strict && !this._longMonthsParse[i]) { + this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); + this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); } - /* falls through */ - case 'SS': - if (strict) { - return parseTokenTwoDigits; + if (!strict && !this._monthsParse[i]) { + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); } - /* falls through */ - case 'SSS': - if (strict) { - return parseTokenThreeDigits; + // test the regex + if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { + return i; + } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { + return i; + } else if (!strict && this._monthsParse[i].test(monthName)) { + return i; } - /* falls through */ - case 'DDD': - return parseTokenOneToThreeDigits; - case 'MMM': - case 'MMMM': - case 'dd': - case 'ddd': - case 'dddd': - return parseTokenWord; - case 'a': - case 'A': - return config._locale._meridiemParse; - case 'X': - return parseTokenTimestampMs; - case 'Z': - case 'ZZ': - return parseTokenTimezone; - case 'T': - return parseTokenT; - case 'SSSS': - return parseTokenDigits; - case 'MM': - case 'DD': - case 'YY': - case 'GG': - case 'gg': - case 'HH': - case 'hh': - case 'mm': - case 'ss': - case 'ww': - case 'WW': - return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits; - case 'M': - case 'D': - case 'd': - case 'H': - case 'h': - case 'm': - case 's': - case 'w': - case 'W': - case 'e': - case 'E': - return parseTokenOneOrTwoDigits; - case 'Do': - return parseTokenOrdinal; - default : - a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), 'i')); + } + } + + // MOMENTS + + function setMonth (mom, value) { + var dayOfMonth; + + if (!mom.isValid()) { + // No op + return mom; + } + + // TODO: Move this out of here! + if (typeof value === 'string') { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (typeof value !== 'number') { + return mom; + } + } + + dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; + } + + function getSetMonth (value) { + if (value != null) { + setMonth(this, value); + utils_hooks__hooks.updateOffset(this, true); + return this; + } else { + return get_set__get(this, 'Month'); + } + } + + function getDaysInMonth () { + return daysInMonth(this.year(), this.month()); + } + + var defaultMonthsShortRegex = matchWord; + function monthsShortRegex (isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsShortStrictRegex; + } else { + return this._monthsShortRegex; + } + } else { + return this._monthsShortStrictRegex && isStrict ? + this._monthsShortStrictRegex : this._monthsShortRegex; + } + } + + var defaultMonthsRegex = matchWord; + function monthsRegex (isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsStrictRegex; + } else { + return this._monthsRegex; + } + } else { + return this._monthsStrictRegex && isStrict ? + this._monthsStrictRegex : this._monthsRegex; + } + } + + function computeMonthsParse () { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var shortPieces = [], longPieces = [], mixedPieces = [], + i, mom; + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = create_utc__createUTC([2000, i]); + shortPieces.push(this.monthsShort(mom, '')); + longPieces.push(this.months(mom, '')); + mixedPieces.push(this.months(mom, '')); + mixedPieces.push(this.monthsShort(mom, '')); + } + // Sorting makes sure if one month (or abbr) is a prefix of another it + // will match the longer piece. + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + for (i = 0; i < 12; i++) { + shortPieces[i] = regexEscape(shortPieces[i]); + longPieces[i] = regexEscape(longPieces[i]); + mixedPieces[i] = regexEscape(mixedPieces[i]); + } + + this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._monthsShortRegex = this._monthsRegex; + this._monthsStrictRegex = new RegExp('^(' + longPieces.join('|') + ')$', 'i'); + this._monthsShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')$', 'i'); + } + + function checkOverflow (m) { + var overflow; + var a = m._a; + + if (a && getParsingFlags(m).overflow === -2) { + overflow = + a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : + a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : + a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : + a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : + a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : + a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } + if (getParsingFlags(m)._overflowWeeks && overflow === -1) { + overflow = WEEK; + } + if (getParsingFlags(m)._overflowWeekday && overflow === -1) { + overflow = WEEKDAY; + } + + getParsingFlags(m).overflow = overflow; + } + + return m; + } + + function warn(msg) { + if (utils_hooks__hooks.suppressDeprecationWarnings === false && + (typeof console !== 'undefined') && console.warn) { + console.warn('Deprecation warning: ' + msg); + } + } + + function deprecate(msg, fn) { + var firstTime = true; + + return extend(function () { + if (firstTime) { + warn(msg + '\nArguments: ' + Array.prototype.slice.call(arguments).join(', ') + '\n' + (new Error()).stack); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); + } + + var deprecations = {}; + + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; + } + } + + utils_hooks__hooks.suppressDeprecationWarnings = false; + + // iso 8601 regex + // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) + var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/; + var basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/; + + var tzRegex = /Z|[+-]\d\d(?::?\d\d)?/; + + var isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/], + ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/], + ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/], + ['GGGG-[W]WW', /\d{4}-W\d\d/, false], + ['YYYY-DDD', /\d{4}-\d{3}/], + ['YYYY-MM', /\d{4}-\d\d/, false], + ['YYYYYYMMDD', /[+-]\d{10}/], + ['YYYYMMDD', /\d{8}/], + // YYYYMM is NOT allowed by the standard + ['GGGG[W]WWE', /\d{4}W\d{3}/], + ['GGGG[W]WW', /\d{4}W\d{2}/, false], + ['YYYYDDD', /\d{7}/] + ]; + + // iso time formats and regexes + var isoTimes = [ + ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/], + ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/], + ['HH:mm:ss', /\d\d:\d\d:\d\d/], + ['HH:mm', /\d\d:\d\d/], + ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/], + ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/], + ['HHmmss', /\d\d\d\d\d\d/], + ['HHmm', /\d\d\d\d/], + ['HH', /\d\d/] + ]; + + var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; + + // date from iso format + function configFromISO(config) { + var i, l, + string = config._i, + match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string), + allowTime, dateFormat, timeFormat, tzFormat; + + if (match) { + getParsingFlags(config).iso = true; + + for (i = 0, l = isoDates.length; i < l; i++) { + if (isoDates[i][1].exec(match[1])) { + dateFormat = isoDates[i][0]; + allowTime = isoDates[i][2] !== false; + break; + } + } + if (dateFormat == null) { + config._isValid = false; + return; + } + if (match[3]) { + for (i = 0, l = isoTimes.length; i < l; i++) { + if (isoTimes[i][1].exec(match[3])) { + // match[2] should be 'T' or space + timeFormat = (match[2] || ' ') + isoTimes[i][0]; + break; + } + } + if (timeFormat == null) { + config._isValid = false; + return; + } + } + if (!allowTime && timeFormat != null) { + config._isValid = false; + return; + } + if (match[4]) { + if (tzRegex.exec(match[4])) { + tzFormat = 'Z'; + } else { + config._isValid = false; + return; + } + } + config._f = dateFormat + (timeFormat || '') + (tzFormat || ''); + configFromStringAndFormat(config); + } else { + config._isValid = false; + } + } + + // date from iso format or fallback + function configFromString(config) { + var matched = aspNetJsonRegex.exec(config._i); + + if (matched !== null) { + config._d = new Date(+matched[1]); + return; + } + + configFromISO(config); + if (config._isValid === false) { + delete config._isValid; + utils_hooks__hooks.createFromInputFallback(config); + } + } + + utils_hooks__hooks.createFromInputFallback = deprecate( + 'moment construction falls back to js Date. This is ' + + 'discouraged and will be removed in upcoming major ' + + 'release. Please refer to ' + + 'https://github.com/moment/moment/issues/1407 for more info.', + function (config) { + config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + } + ); + + function createDate (y, m, d, h, M, s, ms) { + //can't just apply() to create a date: + //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply + var date = new Date(y, m, d, h, M, s, ms); + + //the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0 && isFinite(date.getFullYear())) { + date.setFullYear(y); + } + return date; + } + + function createUTCDate (y) { + var date = new Date(Date.UTC.apply(null, arguments)); + + //the Date.UTC function remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0 && isFinite(date.getUTCFullYear())) { + date.setUTCFullYear(y); + } + return date; + } + + // FORMATTING + + addFormatToken('Y', 0, 0, function () { + var y = this.year(); + return y <= 9999 ? '' + y : '+' + y; + }); + + addFormatToken(0, ['YY', 2], 0, function () { + return this.year() % 100; + }); + + addFormatToken(0, ['YYYY', 4], 0, 'year'); + addFormatToken(0, ['YYYYY', 5], 0, 'year'); + addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); + + // ALIASES + + addUnitAlias('year', 'y'); + + // PARSING + + addRegexToken('Y', matchSigned); + addRegexToken('YY', match1to2, match2); + addRegexToken('YYYY', match1to4, match4); + addRegexToken('YYYYY', match1to6, match6); + addRegexToken('YYYYYY', match1to6, match6); + + addParseToken(['YYYYY', 'YYYYYY'], YEAR); + addParseToken('YYYY', function (input, array) { + array[YEAR] = input.length === 2 ? utils_hooks__hooks.parseTwoDigitYear(input) : toInt(input); + }); + addParseToken('YY', function (input, array) { + array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); + }); + addParseToken('Y', function (input, array) { + array[YEAR] = parseInt(input, 10); + }); + + // HELPERS + + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } + + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } + + // HOOKS + + utils_hooks__hooks.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; + + // MOMENTS + + var getSetYear = makeGetSet('FullYear', false); + + function getIsLeapYear () { + return isLeapYear(this.year()); + } + + // start-of-first-week - start-of-year + function firstWeekOffset(year, dow, doy) { + var // first-week day -- which january is always in the first week (4 for iso, 1 for other) + fwd = 7 + dow - doy, + // first-week day local weekday -- which local weekday is fwd + fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7; + + return -fwdlw + fwd - 1; + } + + //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday + function dayOfYearFromWeeks(year, week, weekday, dow, doy) { + var localWeekday = (7 + weekday - dow) % 7, + weekOffset = firstWeekOffset(year, dow, doy), + dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset, + resYear, resDayOfYear; + + if (dayOfYear <= 0) { + resYear = year - 1; + resDayOfYear = daysInYear(resYear) + dayOfYear; + } else if (dayOfYear > daysInYear(year)) { + resYear = year + 1; + resDayOfYear = dayOfYear - daysInYear(year); + } else { + resYear = year; + resDayOfYear = dayOfYear; + } + + return { + year: resYear, + dayOfYear: resDayOfYear + }; + } + + function weekOfYear(mom, dow, doy) { + var weekOffset = firstWeekOffset(mom.year(), dow, doy), + week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1, + resWeek, resYear; + + if (week < 1) { + resYear = mom.year() - 1; + resWeek = week + weeksInYear(resYear, dow, doy); + } else if (week > weeksInYear(mom.year(), dow, doy)) { + resWeek = week - weeksInYear(mom.year(), dow, doy); + resYear = mom.year() + 1; + } else { + resYear = mom.year(); + resWeek = week; + } + + return { + week: resWeek, + year: resYear + }; + } + + function weeksInYear(year, dow, doy) { + var weekOffset = firstWeekOffset(year, dow, doy), + weekOffsetNext = firstWeekOffset(year + 1, dow, doy); + return (daysInYear(year) - weekOffset + weekOffsetNext) / 7; + } + + // Pick the first defined of two or three arguments. + function defaults(a, b, c) { + if (a != null) { return a; } - } - - function timezoneMinutesFromString(string) { - string = string || ''; - var possibleTzMatches = (string.match(parseTokenTimezone) || []), - tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [], - parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0], - minutes = +(parts[1] * 60) + toInt(parts[2]); - - return parts[0] === '+' ? -minutes : minutes; - } - - // function to convert string input to date - function addTimeToArrayFromToken(token, input, config) { - var a, datePartArray = config._a; - - switch (token) { - // QUARTER - case 'Q': - if (input != null) { - datePartArray[MONTH] = (toInt(input) - 1) * 3; - } - break; - // MONTH - case 'M' : // fall through to MM - case 'MM' : - if (input != null) { - datePartArray[MONTH] = toInt(input) - 1; - } - break; - case 'MMM' : // fall through to MMMM - case 'MMMM' : - a = config._locale.monthsParse(input); - // if we didn't find a month name, mark the date as invalid. - if (a != null) { - datePartArray[MONTH] = a; - } else { - config._pf.invalidMonth = input; - } - break; - // DAY OF MONTH - case 'D' : // fall through to DD - case 'DD' : - if (input != null) { - datePartArray[DATE] = toInt(input); - } - break; - case 'Do' : - if (input != null) { - datePartArray[DATE] = toInt(parseInt(input, 10)); - } - break; - // DAY OF YEAR - case 'DDD' : // fall through to DDDD - case 'DDDD' : - if (input != null) { - config._dayOfYear = toInt(input); - } - - break; - // YEAR - case 'YY' : - datePartArray[YEAR] = moment.parseTwoDigitYear(input); - break; - case 'YYYY' : - case 'YYYYY' : - case 'YYYYYY' : - datePartArray[YEAR] = toInt(input); - break; - // AM / PM - case 'a' : // fall through to A - case 'A' : - config._isPm = config._locale.isPM(input); - break; - // 24 HOUR - case 'H' : // fall through to hh - case 'HH' : // fall through to hh - case 'h' : // fall through to hh - case 'hh' : - datePartArray[HOUR] = toInt(input); - break; - // MINUTE - case 'm' : // fall through to mm - case 'mm' : - datePartArray[MINUTE] = toInt(input); - break; - // SECOND - case 's' : // fall through to ss - case 'ss' : - datePartArray[SECOND] = toInt(input); - break; - // MILLISECOND - case 'S' : - case 'SS' : - case 'SSS' : - case 'SSSS' : - datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000); - break; - // UNIX TIMESTAMP WITH MS - case 'X': - config._d = new Date(parseFloat(input) * 1000); - break; - // TIMEZONE - case 'Z' : // fall through to ZZ - case 'ZZ' : - config._useUTC = true; - config._tzm = timezoneMinutesFromString(input); - break; - // WEEKDAY - human - case 'dd': - case 'ddd': - case 'dddd': - a = config._locale.weekdaysParse(input); - // if we didn't get a weekday name, mark the date as invalid - if (a != null) { - config._w = config._w || {}; - config._w['d'] = a; - } else { - config._pf.invalidWeekday = input; - } - break; - // WEEK, WEEK DAY - numeric - case 'w': - case 'ww': - case 'W': - case 'WW': - case 'd': - case 'e': - case 'E': - token = token.substr(0, 1); - /* falls through */ - case 'gggg': - case 'GGGG': - case 'GGGGG': - token = token.substr(0, 2); - if (input) { - config._w = config._w || {}; - config._w[token] = toInt(input); - } - break; - case 'gg': - case 'GG': - config._w = config._w || {}; - config._w[token] = moment.parseTwoDigitYear(input); + if (b != null) { + return b; } + return c; } - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; - - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; - - // TODO: We need to take the current isoWeekYear, but that depends on - // how we interpret now (local, utc, fixed offset). So create - // a now version of current config (take local/utc/offset flags, and - // create now). - weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year); - week = dfl(w.W, 1); - weekday = dfl(w.E, 1); - } else { - dow = config._locale._week.dow; - doy = config._locale._week.doy; - - weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); - week = dfl(w.w, 1); - - if (w.d != null) { - // weekday -- low day numbers are considered next week - weekday = w.d; - if (weekday < dow) { - ++week; - } - } else if (w.e != null) { - // local weekday -- counting starts from begining of week - weekday = w.e + dow; - } else { - // default to begining of week - weekday = dow; - } + function currentDateArray(config) { + // hooks is actually the exported moment object + var nowValue = new Date(utils_hooks__hooks.now()); + if (config._useUTC) { + return [nowValue.getUTCFullYear(), nowValue.getUTCMonth(), nowValue.getUTCDate()]; } - temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); - - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; + return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()]; } // convert an array to a date. // the array should mirror the parameters below // note: all values past the year are optional and will default to the lowest possible value. // [year, month, day , hour, minute, second, millisecond] - function dateFromConfig(config) { + function configFromArray (config) { var i, date, input = [], currentDate, yearToUse; if (config._d) { @@ -1329,13 +1167,13 @@ //if the day of the year is set, figure out what it is if (config._dayOfYear) { - yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); + yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); if (config._dayOfYear > daysInYear(yearToUse)) { - config._pf._overflowDayOfYear = true; + getParsingFlags(config)._overflowDayOfYear = true; } - date = makeUTCDate(yearToUse, 0, config._dayOfYear); + date = createUTCDate(yearToUse, 0, config._dayOfYear); config._a[MONTH] = date.getUTCMonth(); config._a[DATE] = date.getUTCDate(); } @@ -1354,57 +1192,93 @@ config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; } - config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); - // Apply timezone offset from input. The actual zone can be changed + // Check for 24:00:00.000 + if (config._a[HOUR] === 24 && + config._a[MINUTE] === 0 && + config._a[SECOND] === 0 && + config._a[MILLISECOND] === 0) { + config._nextDay = true; + config._a[HOUR] = 0; + } + + config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); + // Apply timezone offset from input. The actual utcOffset can be changed // with parseZone. if (config._tzm != null) { - config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm); + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + } + + if (config._nextDay) { + config._a[HOUR] = 24; } } - function dateFromObject(config) { - var normalizedInput; + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow; - if (config._d) { - return; - } + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - normalizedInput = normalizeObjectUnits(config._i); - config._a = [ - normalizedInput.year, - normalizedInput.month, - normalizedInput.day, - normalizedInput.hour, - normalizedInput.minute, - normalizedInput.second, - normalizedInput.millisecond - ]; - - dateFromConfig(config); - } - - function currentDateArray(config) { - var now = new Date(); - if (config._useUTC) { - return [ - now.getUTCFullYear(), - now.getUTCMonth(), - now.getUTCDate() - ]; + // TODO: We need to take the current isoWeekYear, but that depends on + // how we interpret now (local, utc, fixed offset). So create + // a now version of current config (take local/utc/offset flags, and + // create now). + weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year); + week = defaults(w.W, 1); + weekday = defaults(w.E, 1); + if (weekday < 1 || weekday > 7) { + weekdayOverflow = true; + } } else { - return [now.getFullYear(), now.getMonth(), now.getDate()]; + dow = config._locale._week.dow; + doy = config._locale._week.doy; + + weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); + week = defaults(w.w, 1); + + if (w.d != null) { + // weekday -- low day numbers are considered next week + weekday = w.d; + if (weekday < 0 || weekday > 6) { + weekdayOverflow = true; + } + } else if (w.e != null) { + // local weekday -- counting starts from begining of week + weekday = w.e + dow; + if (w.e < 0 || w.e > 6) { + weekdayOverflow = true; + } + } else { + // default to begining of week + weekday = dow; + } + } + if (week < 1 || week > weeksInYear(weekYear, dow, doy)) { + getParsingFlags(config)._overflowWeeks = true; + } else if (weekdayOverflow != null) { + getParsingFlags(config)._overflowWeekday = true; + } else { + temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy); + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; } } + // constant that refers to the ISO standard + utils_hooks__hooks.ISO_8601 = function () {}; + // date from string and format string - function makeDateFromStringAndFormat(config) { - if (config._f === moment.ISO_8601) { - parseISO(config); + function configFromStringAndFormat(config) { + // TODO: Move this to another part of the creation flow to prevent circular deps + if (config._f === utils_hooks__hooks.ISO_8601) { + configFromISO(config); return; } config._a = []; - config._pf.empty = true; + getParsingFlags(config).empty = true; // This array is used to make a Date, either with `new Date` or `Date.UTC` var string = '' + config._i, @@ -1417,10 +1291,12 @@ for (i = 0; i < tokens.length; i++) { token = tokens[i]; parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; + // console.log('token', token, 'parsedInput', parsedInput, + // 'regex', getParseRegexForToken(token, config)); if (parsedInput) { skipped = string.substr(0, string.indexOf(parsedInput)); if (skipped.length > 0) { - config._pf.unusedInput.push(skipped); + getParsingFlags(config).unusedInput.push(skipped); } string = string.slice(string.indexOf(parsedInput) + parsedInput.length); totalParsedInputLength += parsedInput.length; @@ -1428,50 +1304,65 @@ // don't parse if it's not a known token if (formatTokenFunctions[token]) { if (parsedInput) { - config._pf.empty = false; + getParsingFlags(config).empty = false; } else { - config._pf.unusedTokens.push(token); + getParsingFlags(config).unusedTokens.push(token); } addTimeToArrayFromToken(token, parsedInput, config); } else if (config._strict && !parsedInput) { - config._pf.unusedTokens.push(token); + getParsingFlags(config).unusedTokens.push(token); } } // add remaining unparsed input length to the string - config._pf.charsLeftOver = stringLength - totalParsedInputLength; + getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; if (string.length > 0) { - config._pf.unusedInput.push(string); + getParsingFlags(config).unusedInput.push(string); } - // handle am pm - if (config._isPm && config._a[HOUR] < 12) { - config._a[HOUR] += 12; - } - // if is 12 am, change hours to 0 - if (config._isPm === false && config._a[HOUR] === 12) { - config._a[HOUR] = 0; + // clear _12h flag if hour is <= 12 + if (getParsingFlags(config).bigHour === true && + config._a[HOUR] <= 12 && + config._a[HOUR] > 0) { + getParsingFlags(config).bigHour = undefined; } + // handle meridiem + config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); - dateFromConfig(config); + configFromArray(config); checkOverflow(config); } - function unescapeFormat(s) { - return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }); - } - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function regexpEscape(s) { - return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + function meridiemFixWrap (locale, hour, meridiem) { + var isPm; + + if (meridiem == null) { + // nothing to do + return hour; + } + if (locale.meridiemHour != null) { + return locale.meridiemHour(hour, meridiem); + } else if (locale.isPM != null) { + // Fallback + isPm = locale.isPM(meridiem); + if (isPm && hour < 12) { + hour += 12; + } + if (!isPm && hour === 12) { + hour = 0; + } + return hour; + } else { + // this is not supposed to happen + return hour; + } } // date from string and array of format strings - function makeDateFromStringAndArray(config) { + function configFromStringAndArray(config) { var tempConfig, bestMoment, @@ -1480,7 +1371,7 @@ currentScore; if (config._f.length === 0) { - config._pf.invalidFormat = true; + getParsingFlags(config).invalidFormat = true; config._d = new Date(NaN); return; } @@ -1488,21 +1379,23 @@ for (i = 0; i < config._f.length; i++) { currentScore = 0; tempConfig = copyConfig({}, config); - tempConfig._pf = defaultParsingFlags(); + if (config._useUTC != null) { + tempConfig._useUTC = config._useUTC; + } tempConfig._f = config._f[i]; - makeDateFromStringAndFormat(tempConfig); + configFromStringAndFormat(tempConfig); - if (!isValid(tempConfig)) { + if (!valid__isValid(tempConfig)) { continue; } // if there is any input that was not parsed add a penalty for that format - currentScore += tempConfig._pf.charsLeftOver; + currentScore += getParsingFlags(tempConfig).charsLeftOver; //or tokens - currentScore += tempConfig._pf.unusedTokens.length * 10; + currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; - tempConfig._pf.score = currentScore; + getParsingFlags(tempConfig).score = currentScore; if (scoreToBeat == null || currentScore < scoreToBeat) { scoreToBeat = currentScore; @@ -1513,251 +1406,130 @@ extend(config, bestMoment || tempConfig); } - // date from iso format - function parseISO(config) { - var i, l, - string = config._i, - match = isoRegex.exec(string); - - if (match) { - config._pf.iso = true; - for (i = 0, l = isoDates.length; i < l; i++) { - if (isoDates[i][1].exec(string)) { - // match[5] should be "T" or undefined - config._f = isoDates[i][0] + (match[6] || ' '); - break; - } - } - for (i = 0, l = isoTimes.length; i < l; i++) { - if (isoTimes[i][1].exec(string)) { - config._f += isoTimes[i][0]; - break; - } - } - if (string.match(parseTokenTimezone)) { - config._f += 'Z'; - } - makeDateFromStringAndFormat(config); - } else { - config._isValid = false; - } - } - - // date from iso format or fallback - function makeDateFromString(config) { - parseISO(config); - if (config._isValid === false) { - delete config._isValid; - moment.createFromInputFallback(config); - } - } - - function makeDateFromInput(config) { - var input = config._i, matched; - if (input === undefined) { - config._d = new Date(); - } else if (isDate(input)) { - config._d = new Date(+input); - } else if ((matched = aspNetJsonRegex.exec(input)) !== null) { - config._d = new Date(+matched[1]); - } else if (typeof input === 'string') { - makeDateFromString(config); - } else if (isArray(input)) { - config._a = input.slice(0); - dateFromConfig(config); - } else if (typeof(input) === 'object') { - dateFromObject(config); - } else if (typeof(input) === 'number') { - // from milliseconds - config._d = new Date(input); - } else { - moment.createFromInputFallback(config); - } - } - - function makeDate(y, m, d, h, M, s, ms) { - //can't just apply() to create a date: - //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply - var date = new Date(y, m, d, h, M, s, ms); - - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } - - function makeUTCDate(y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; - } - - function parseWeekday(input, locale) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = locale.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; - } - - /************************************ - Relative Time - ************************************/ - - - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); - } - - function relativeTime(posNegDuration, withoutSuffix, locale) { - var duration = moment.duration(posNegDuration).abs(), - seconds = round(duration.as('s')), - minutes = round(duration.as('m')), - hours = round(duration.as('h')), - days = round(duration.as('d')), - months = round(duration.as('M')), - years = round(duration.as('y')), - - args = seconds < relativeTimeThresholds.s && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < relativeTimeThresholds.m && ['mm', minutes] || - hours === 1 && ['h'] || - hours < relativeTimeThresholds.h && ['hh', hours] || - days === 1 && ['d'] || - days < relativeTimeThresholds.d && ['dd', days] || - months === 1 && ['M'] || - months < relativeTimeThresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; - - args[2] = withoutSuffix; - args[3] = +posNegDuration > 0; - args[4] = locale; - return substituteTimeAgo.apply({}, args); - } - - - /************************************ - Week of Year - ************************************/ - - - // firstDayOfWeek 0 = sun, 6 = sat - // the day of the week that starts the week - // (usually sunday or monday) - // firstDayOfWeekOfYear 0 = sun, 6 = sat - // the first week is the week that contains the first - // of this day of the week - // (eg. ISO weeks use thursday (4)) - function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { - var end = firstDayOfWeekOfYear - firstDayOfWeek, - daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), - adjustedMoment; - - - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; + function configFromObject(config) { + if (config._d) { + return; } - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; + var i = normalizeObjectUnits(config._i); + config._a = map([i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond], function (obj) { + return obj && parseInt(obj, 10); + }); + + configFromArray(config); + } + + function createFromConfig (config) { + var res = new Moment(checkOverflow(prepareConfig(config))); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; } - adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; + return res; } - //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday - function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { - var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear; - - d = d === 0 ? 7 : d; - weekday = weekday != null ? weekday : firstDayOfWeek; - daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); - dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; - - return { - year: dayOfYear > 0 ? year : year - 1, - dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } - - /************************************ - Top Level Functions - ************************************/ - - function makeMoment(config) { + function prepareConfig (config) { var input = config._i, format = config._f; - config._locale = config._locale || moment.localeData(config._l); + config._locale = config._locale || locale_locales__getLocale(config._l); if (input === null || (format === undefined && input === '')) { - return moment.invalid({nullInput: true}); + return valid__createInvalid({nullInput: true}); } if (typeof input === 'string') { config._i = input = config._locale.preparse(input); } - if (moment.isMoment(input)) { - return new Moment(input, true); + if (isMoment(input)) { + return new Moment(checkOverflow(input)); + } else if (isArray(format)) { + configFromStringAndArray(config); } else if (format) { - if (isArray(format)) { - makeDateFromStringAndArray(config); - } else { - makeDateFromStringAndFormat(config); - } + configFromStringAndFormat(config); + } else if (isDate(input)) { + config._d = input; } else { - makeDateFromInput(config); + configFromInput(config); } - return new Moment(config); + if (!valid__isValid(config)) { + config._d = null; + } + + return config; } - moment = function (input, format, locale, strict) { - var c; + function configFromInput(config) { + var input = config._i; + if (input === undefined) { + config._d = new Date(utils_hooks__hooks.now()); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if (typeof input === 'string') { + configFromString(config); + } else if (isArray(input)) { + config._a = map(input.slice(0), function (obj) { + return parseInt(obj, 10); + }); + configFromArray(config); + } else if (typeof(input) === 'object') { + configFromObject(config); + } else if (typeof(input) === 'number') { + // from milliseconds + config._d = new Date(input); + } else { + utils_hooks__hooks.createFromInputFallback(config); + } + } - if (typeof(locale) === "boolean") { + function createLocalOrUTC (input, format, locale, strict, isUTC) { + var c = {}; + + if (typeof(locale) === 'boolean') { strict = locale; locale = undefined; } // object construction must be done this way. // https://github.com/moment/moment/issues/1423 - c = {}; c._isAMomentObject = true; + c._useUTC = c._isUTC = isUTC; + c._l = locale; c._i = input; c._f = format; - c._l = locale; c._strict = strict; - c._isUTC = false; - c._pf = defaultParsingFlags(); - return makeMoment(c); - }; + return createFromConfig(c); + } - moment.suppressDeprecationWarnings = false; + function local__createLocal (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, false); + } - moment.createFromInputFallback = deprecate( - 'moment construction falls back to js Date. This is ' + - 'discouraged and will be removed in upcoming major ' + - 'release. Please refer to ' + - 'https://github.com/moment/moment/issues/1407 for more info.', - function (config) { - config._d = new Date(config._i); + var prototypeMin = deprecate( + 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', + function () { + var other = local__createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other < this ? this : other; + } else { + return valid__createInvalid(); + } + } + ); + + var prototypeMax = deprecate( + 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', + function () { + var other = local__createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other > this ? this : other; + } else { + return valid__createInvalid(); + } } ); @@ -1772,72 +1544,297 @@ moments = moments[0]; } if (!moments.length) { - return moment(); + return local__createLocal(); } res = moments[0]; for (i = 1; i < moments.length; ++i) { - if (moments[i][fn](res)) { + if (!moments[i].isValid() || moments[i][fn](res)) { res = moments[i]; } } return res; } - moment.min = function () { + // TODO: Use [].sort instead? + function min () { var args = [].slice.call(arguments, 0); return pickBy('isBefore', args); - }; + } - moment.max = function () { + function max () { var args = [].slice.call(arguments, 0); return pickBy('isAfter', args); + } + + var now = function () { + return Date.now ? Date.now() : +(new Date()); }; - // creating with utc - moment.utc = function (input, format, locale, strict) { - var c; + function Duration (duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + quarters = normalizedInput.quarter || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; - if (typeof(locale) === "boolean") { - strict = locale; - locale = undefined; + // representation for dateAddRemove + this._milliseconds = +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 36e5; // 1000 * 60 * 60 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + + weeks * 7; + // It is impossible translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + + quarters * 3 + + years * 12; + + this._data = {}; + + this._locale = locale_locales__getLocale(); + + this._bubble(); + } + + function isDuration (obj) { + return obj instanceof Duration; + } + + // FORMATTING + + function offset (token, separator) { + addFormatToken(token, 0, 0, function () { + var offset = this.utcOffset(); + var sign = '+'; + if (offset < 0) { + offset = -offset; + sign = '-'; + } + return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); + }); + } + + offset('Z', ':'); + offset('ZZ', ''); + + // PARSING + + addRegexToken('Z', matchShortOffset); + addRegexToken('ZZ', matchShortOffset); + addParseToken(['Z', 'ZZ'], function (input, array, config) { + config._useUTC = true; + config._tzm = offsetFromString(matchShortOffset, input); + }); + + // HELPERS + + // timezone chunker + // '+10:00' > ['10', '00'] + // '-1530' > ['-15', '30'] + var chunkOffset = /([\+\-]|\d\d)/gi; + + function offsetFromString(matcher, string) { + var matches = ((string || '').match(matcher) || []); + var chunk = matches[matches.length - 1] || []; + var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; + var minutes = +(parts[1] * 60) + toInt(parts[2]); + + return parts[0] === '+' ? minutes : -minutes; + } + + // Return a moment from input, that is local/utc/zone equivalent to model. + function cloneWithOffset(input, model) { + var res, diff; + if (model._isUTC) { + res = model.clone(); + diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); + // Use low-level api, because this fn is low-level api. + res._d.setTime(+res._d + diff); + utils_hooks__hooks.updateOffset(res, false); + return res; + } else { + return local__createLocal(input).local(); } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c = {}; - c._isAMomentObject = true; - c._useUTC = true; - c._isUTC = true; - c._l = locale; - c._i = input; - c._f = format; - c._strict = strict; - c._pf = defaultParsingFlags(); + } - return makeMoment(c).utc(); - }; + function getDateOffset (m) { + // On Firefox.24 Date#getTimezoneOffset returns a floating point. + // https://github.com/moment/moment/pull/1871 + return -Math.round(m._d.getTimezoneOffset() / 15) * 15; + } - // creating with unix timestamp (in seconds) - moment.unix = function (input) { - return moment(input * 1000); - }; + // HOOKS - // duration - moment.duration = function (input, key) { + // This function will be called whenever a moment is mutated. + // It is intended to keep the offset in sync with the timezone. + utils_hooks__hooks.updateOffset = function () {}; + + // MOMENTS + + // keepLocalTime = true means only change the timezone, without + // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + function getSetOffset (input, keepLocalTime) { + var offset = this._offset || 0, + localAdjust; + if (!this.isValid()) { + return input != null ? this : NaN; + } + if (input != null) { + if (typeof input === 'string') { + input = offsetFromString(matchShortOffset, input); + } else if (Math.abs(input) < 16) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = getDateOffset(this); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + utils_hooks__hooks.updateOffset(this, true); + this._changeInProgress = null; + } + } + return this; + } else { + return this._isUTC ? offset : getDateOffset(this); + } + } + + function getSetZone (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } + + this.utcOffset(input, keepLocalTime); + + return this; + } else { + return -this.utcOffset(); + } + } + + function setOffsetToUTC (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + } + + function setOffsetToLocal (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; + + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } + } + return this; + } + + function setOffsetToParsedOffset () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(offsetFromString(matchOffset, this._i)); + } + return this; + } + + function hasAlignedHourOffset (input) { + if (!this.isValid()) { + return false; + } + input = input ? local__createLocal(input).utcOffset() : 0; + + return (this.utcOffset() - input) % 60 === 0; + } + + function isDaylightSavingTime () { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); + } + + function isDaylightSavingTimeShifted () { + if (!isUndefined(this._isDSTShifted)) { + return this._isDSTShifted; + } + + var c = {}; + + copyConfig(c, this); + c = prepareConfig(c); + + if (c._a) { + var other = c._isUTC ? create_utc__createUTC(c._a) : local__createLocal(c._a); + this._isDSTShifted = this.isValid() && + compareArrays(c._a, other.toArray()) > 0; + } else { + this._isDSTShifted = false; + } + + return this._isDSTShifted; + } + + function isLocal () { + return this.isValid() ? !this._isUTC : false; + } + + function isUtcOffset () { + return this.isValid() ? this._isUTC : false; + } + + function isUtc () { + return this.isValid() ? this._isUTC && this._offset === 0 : false; + } + + // ASP.NET json date format regex + var aspNetRegex = /^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/; + + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + var isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; + + function create__createDuration (input, key) { var duration = input, // matching against regexp is expensive, do it on demand match = null, sign, ret, - parseIso, diffRes; - if (moment.isDuration(input)) { + if (isDuration(input)) { duration = { - ms: input._milliseconds, - d: input._days, - M: input._months + ms : input._milliseconds, + d : input._days, + M : input._months }; } else if (typeof input === 'number') { duration = {}; @@ -1846,38 +1843,31 @@ } else { duration.milliseconds = input; } - } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) { + } else if (!!(match = aspNetRegex.exec(input))) { sign = (match[1] === '-') ? -1 : 1; duration = { - y: 0, - d: toInt(match[DATE]) * sign, - h: toInt(match[HOUR]) * sign, - m: toInt(match[MINUTE]) * sign, - s: toInt(match[SECOND]) * sign, - ms: toInt(match[MILLISECOND]) * sign + y : 0, + d : toInt(match[DATE]) * sign, + h : toInt(match[HOUR]) * sign, + m : toInt(match[MINUTE]) * sign, + s : toInt(match[SECOND]) * sign, + ms : toInt(match[MILLISECOND]) * sign }; - } else if (!!(match = isoDurationRegex.exec(input))) { + } else if (!!(match = isoRegex.exec(input))) { sign = (match[1] === '-') ? -1 : 1; - parseIso = function (inp) { - // We'd normally use ~~inp for this, but unfortunately it also - // converts floats to ints. - // inp may be undefined, so careful calling replace on it. - var res = inp && parseFloat(inp.replace(',', '.')); - // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; - }; duration = { - y: parseIso(match[2]), - M: parseIso(match[3]), - d: parseIso(match[4]), - h: parseIso(match[5]), - m: parseIso(match[6]), - s: parseIso(match[7]), - w: parseIso(match[8]) + y : parseIso(match[2], sign), + M : parseIso(match[3], sign), + d : parseIso(match[4], sign), + h : parseIso(match[5], sign), + m : parseIso(match[6], sign), + s : parseIso(match[7], sign), + w : parseIso(match[8], sign) }; - } else if (typeof duration === 'object' && - ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(moment(duration.from), moment(duration.to)); + } else if (duration == null) {// checks for null or undefined + duration = {}; + } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { + diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); duration = {}; duration.ms = diffRes.milliseconds; @@ -1886,876 +1876,1335 @@ ret = new Duration(duration); - if (moment.isDuration(input) && input.hasOwnProperty('_locale')) { + if (isDuration(input) && hasOwnProp(input, '_locale')) { ret._locale = input._locale; } return ret; - }; + } - // version number - moment.version = VERSION; + create__createDuration.fn = Duration.prototype; - // default format - moment.defaultFormat = isoFormat; + function parseIso (inp, sign) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; + } - // constant that refers to the ISO standard - moment.ISO_8601 = function () {}; + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; - // Plugins that add properties should also add the key here (null value), - // so we can properly clone ourselves. - moment.momentProperties = momentProperties; - - // This function will be called whenever a moment is mutated. - // It is intended to keep the offset in sync with the timezone. - moment.updateOffset = function () {}; - - // This function allows you to set a threshold for relative time strings - moment.relativeTimeThreshold = function (threshold, limit) { - if (relativeTimeThresholds[threshold] === undefined) { - return false; - } - if (limit === undefined) { - return relativeTimeThresholds[threshold]; - } - relativeTimeThresholds[threshold] = limit; - return true; - }; - - moment.lang = deprecate( - "moment.lang is deprecated. Use moment.locale instead.", - function (key, value) { - return moment.locale(key, value); - } - ); - - // This function will load locale and then set the global locale. If - // no arguments are passed in, it will simply return the current global - // locale key. - moment.locale = function (key, values) { - var data; - if (key) { - if (typeof(values) !== "undefined") { - data = moment.defineLocale(key, values); - } - else { - data = moment.localeData(key); - } - - if (data) { - moment.duration._locale = moment._locale = data; - } + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; } - return moment._locale._abbr; - }; + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - moment.defineLocale = function (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); + return res; + } - // backwards compat for now: also set the locale - moment.locale(name); + function momentsDifference(base, other) { + var res; + if (!(base.isValid() && other.isValid())) { + return {milliseconds: 0, months: 0}; + } - return locales[name]; + other = cloneWithOffset(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); } else { - // useful for testing - delete locales[name]; - return null; - } - }; - - moment.langData = deprecate( - "moment.langData is deprecated. Use moment.localeData instead.", - function (key) { - return moment.localeData(key); - } - ); - - // returns locale data - moment.localeData = function (key) { - var locale; - - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; } - if (!key) { - return moment._locale; - } - - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; - } - - return chooseLocale(key); - }; - - // compare moment object - moment.isMoment = function (obj) { - return obj instanceof Moment || - (obj != null && obj.hasOwnProperty('_isAMomentObject')); - }; - - // for typechecking Duration objects - moment.isDuration = function (obj) { - return obj instanceof Duration; - }; - - for (i = lists.length - 1; i >= 0; --i) { - makeList(lists[i]); + return res; } - moment.normalizeUnits = function (units) { - return normalizeUnits(units); - }; - - moment.invalid = function (flags) { - var m = moment.utc(NaN); - if (flags != null) { - extend(m._pf, flags); - } - else { - m._pf.userInvalidated = true; - } - - return m; - }; - - moment.parseZone = function () { - return moment.apply(null, arguments).parseZone(); - }; - - moment.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; - - /************************************ - Moment Prototype - ************************************/ - - - extend(moment.fn = Moment.prototype, { - - clone : function () { - return moment(this); - }, - - valueOf : function () { - return +this._d + ((this._offset || 0) * 60000); - }, - - unix : function () { - return Math.floor(+this / 1000); - }, - - toString : function () { - return this.clone().locale('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); - }, - - toDate : function () { - return this._offset ? new Date(+this) : this._d; - }, - - toISOString : function () { - var m = moment(this).utc(); - if (0 < m.year() && m.year() <= 9999) { - return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } else { - return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } - }, - - toArray : function () { - var m = this; - return [ - m.year(), - m.month(), - m.date(), - m.hours(), - m.minutes(), - m.seconds(), - m.milliseconds() - ]; - }, - - isValid : function () { - return isValid(this); - }, - - isDSTShifted : function () { - if (this._a) { - return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + // TODO: remove 'name' arg after deprecation is removed + function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); + tmp = val; val = period; period = tmp; } - return false; - }, - - parsingFlags : function () { - return extend({}, this._pf); - }, - - invalidAt: function () { - return this._pf.overflow; - }, - - utc : function (keepLocalTime) { - return this.zone(0, keepLocalTime); - }, - - local : function (keepLocalTime) { - if (this._isUTC) { - this.zone(0, keepLocalTime); - this._isUTC = false; - - if (keepLocalTime) { - this.add(this._d.getTimezoneOffset(), 'm'); - } - } + val = typeof val === 'string' ? +val : val; + dur = create__createDuration(val, period); + add_subtract__addSubtract(this, dur, direction); return this; - }, - - format : function (inputString) { - var output = formatMoment(this, inputString || moment.defaultFormat); - return this.localeData().postformat(output); - }, - - add : createAdder(1, 'add'), - - subtract : createAdder(-1, 'subtract'), - - diff : function (input, units, asFloat) { - var that = makeAs(input, this), - zoneDiff = (this.zone() - that.zone()) * 6e4, - diff, output; - - units = normalizeUnits(units); - - if (units === 'year' || units === 'month') { - // average number of days in the months in the given dates - diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2 - // difference in months - output = ((this.year() - that.year()) * 12) + (this.month() - that.month()); - // adjust by taking difference in days, average number of days - // and dst in the given months. - output += ((this - moment(this).startOf('month')) - - (that - moment(that).startOf('month'))) / diff; - // same as above but with zones, to negate all dst - output -= ((this.zone() - moment(this).startOf('month').zone()) - - (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff; - if (units === 'year') { - output = output / 12; - } - } else { - diff = (this - that); - output = units === 'second' ? diff / 1e3 : // 1000 - units === 'minute' ? diff / 6e4 : // 1000 * 60 - units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60 - units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst - units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst - diff; - } - return asFloat ? output : absRound(output); - }, - - from : function (time, withoutSuffix) { - return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - }, - - fromNow : function (withoutSuffix) { - return this.from(moment(), withoutSuffix); - }, - - calendar : function (time) { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're zone'd or not. - var now = time || moment(), - sod = makeAs(now, this).startOf('day'), - diff = this.diff(sod, 'days', true), - format = diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; - return this.format(this.localeData().calendar(format, this)); - }, - - isLeapYear : function () { - return isLeapYear(this.year()); - }, - - isDST : function () { - return (this.zone() < this.clone().month(0).zone() || - this.zone() < this.clone().month(5).zone()); - }, - - day : function (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - if (input != null) { - input = parseWeekday(input, this.localeData()); - return this.add(input - day, 'd'); - } else { - return day; - } - }, - - month : makeAccessor('Month', true), - - startOf : function (units) { - units = normalizeUnits(units); - // the following switch intentionally omits break keywords - // to utilize falling through the cases. - switch (units) { - case 'year': - this.month(0); - /* falls through */ - case 'quarter': - case 'month': - this.date(1); - /* falls through */ - case 'week': - case 'isoWeek': - case 'day': - this.hours(0); - /* falls through */ - case 'hour': - this.minutes(0); - /* falls through */ - case 'minute': - this.seconds(0); - /* falls through */ - case 'second': - this.milliseconds(0); - /* falls through */ - } - - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } else if (units === 'isoWeek') { - this.isoWeekday(1); - } - - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } - - return this; - }, - - endOf: function (units) { - units = normalizeUnits(units); - return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); - }, - - isAfter: function (input, units) { - units = typeof units !== 'undefined' ? units : 'millisecond'; - return +this.clone().startOf(units) > +moment(input).startOf(units); - }, - - isBefore: function (input, units) { - units = typeof units !== 'undefined' ? units : 'millisecond'; - return +this.clone().startOf(units) < +moment(input).startOf(units); - }, - - isSame: function (input, units) { - units = units || 'ms'; - return +this.clone().startOf(units) === +makeAs(input, this).startOf(units); - }, - - min: deprecate( - 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', - function (other) { - other = moment.apply(null, arguments); - return other < this ? this : other; - } - ), - - max: deprecate( - 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', - function (other) { - other = moment.apply(null, arguments); - return other > this ? this : other; - } - ), - - // keepLocalTime = true means only change the timezone, without - // affecting the local hour. So 5:31:26 +0300 --[zone(2, true)]--> - // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist int zone - // +0200, so we adjust the time as needed, to be valid. - // - // Keeping the time actually adds/subtracts (one hour) - // from the actual represented time. That is why we call updateOffset - // a second time. In case it wants us to change the offset again - // _changeInProgress == true case, then we have to adjust, because - // there is no such time in the given timezone. - zone : function (input, keepLocalTime) { - var offset = this._offset || 0, - localAdjust; - if (input != null) { - if (typeof input === 'string') { - input = timezoneMinutesFromString(input); - } - if (Math.abs(input) < 16) { - input = input * 60; - } - if (!this._isUTC && keepLocalTime) { - localAdjust = this._d.getTimezoneOffset(); - } - this._offset = input; - this._isUTC = true; - if (localAdjust != null) { - this.subtract(localAdjust, 'm'); - } - if (offset !== input) { - if (!keepLocalTime || this._changeInProgress) { - addOrSubtractDurationFromMoment(this, - moment.duration(offset - input, 'm'), 1, false); - } else if (!this._changeInProgress) { - this._changeInProgress = true; - moment.updateOffset(this, true); - this._changeInProgress = null; - } - } - } else { - return this._isUTC ? offset : this._d.getTimezoneOffset(); - } - return this; - }, - - zoneAbbr : function () { - return this._isUTC ? 'UTC' : ''; - }, - - zoneName : function () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - }, - - parseZone : function () { - if (this._tzm) { - this.zone(this._tzm); - } else if (typeof this._i === 'string') { - this.zone(this._i); - } - return this; - }, - - hasAlignedHourOffset : function (input) { - if (!input) { - input = 0; - } - else { - input = moment(input).zone(); - } - - return (this.zone() - input) % 60 === 0; - }, - - daysInMonth : function () { - return daysInMonth(this.year(), this.month()); - }, - - dayOfYear : function (input) { - var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; - return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); - }, - - quarter : function (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); - }, - - weekYear : function (input) { - var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; - return input == null ? year : this.add((input - year), 'y'); - }, - - isoWeekYear : function (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); - }, - - week : function (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); - }, - - isoWeek : function (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); - }, - - weekday : function (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - }, - - isoWeekday : function (input) { - // behaves the same as moment#day except - // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) - // as a setter, sunday should belong to the previous week. - return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); - }, - - isoWeeksInYear : function () { - return weeksInYear(this.year(), 1, 4); - }, - - weeksInYear : function () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); - }, - - get : function (units) { - units = normalizeUnits(units); - return this[units](); - }, - - set : function (units, value) { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - this[units](value); - } - return this; - }, - - // If passed a locale key, it will set the locale for this - // instance. Otherwise, it will return the locale configuration - // variables for this instance. - locale : function (key) { - if (key === undefined) { - return this._locale._abbr; - } else { - this._locale = moment.localeData(key); - return this; - } - }, - - lang : deprecate( - "moment().lang() is deprecated. Use moment().localeData() instead.", - function (key) { - if (key === undefined) { - return this.localeData(); - } else { - this._locale = moment.localeData(key); - return this; - } - } - ), - - localeData : function () { - return this._locale; - } - }); - - function rawMonthSetter(mom, value) { - var dayOfMonth; - - // TODO: Move this out of here! - if (typeof value === 'string') { - value = mom.localeData().monthsParse(value); - // TODO: Another silent failure? - if (typeof value !== 'number') { - return mom; - } - } - - dayOfMonth = Math.min(mom.date(), - daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; - } - - function rawGetter(mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); - } - - function rawSetter(mom, unit, value) { - if (unit === 'Month') { - return rawMonthSetter(mom, value); - } else { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); - } - } - - function makeAccessor(unit, keepTime) { - return function (value) { - if (value != null) { - rawSetter(this, unit, value); - moment.updateOffset(this, keepTime); - return this; - } else { - return rawGetter(this, unit); - } }; } - moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false); - moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false); - moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false); + function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + + if (!mom.isValid()) { + // No op + return; + } + + updateOffset = updateOffset == null ? true : updateOffset; + + if (milliseconds) { + mom._d.setTime(+mom._d + milliseconds * isAdding); + } + if (days) { + get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); + } + if (months) { + setMonth(mom, get_set__get(mom, 'Month') + months * isAdding); + } + if (updateOffset) { + utils_hooks__hooks.updateOffset(mom, days || months); + } + } + + var add_subtract__add = createAdder(1, 'add'); + var add_subtract__subtract = createAdder(-1, 'subtract'); + + function moment_calendar__calendar (time, formats) { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're local/utc/offset or not. + var now = time || local__createLocal(), + sod = cloneWithOffset(now, this).startOf('day'), + diff = this.diff(sod, 'days', true), + format = diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; + + var output = formats && (isFunction(formats[format]) ? formats[format]() : formats[format]); + + return this.format(output || this.localeData().calendar(format, this, local__createLocal(now))); + } + + function clone () { + return new Moment(this); + } + + function isAfter (input, units) { + var localInput = isMoment(input) ? input : local__createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); + if (units === 'millisecond') { + return +this > +localInput; + } else { + return +localInput < +this.clone().startOf(units); + } + } + + function isBefore (input, units) { + var localInput = isMoment(input) ? input : local__createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); + if (units === 'millisecond') { + return +this < +localInput; + } else { + return +this.clone().endOf(units) < +localInput; + } + } + + function isBetween (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); + } + + function isSame (input, units) { + var localInput = isMoment(input) ? input : local__createLocal(input), + inputMs; + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units || 'millisecond'); + if (units === 'millisecond') { + return +this === +localInput; + } else { + inputMs = +localInput; + return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); + } + } + + function isSameOrAfter (input, units) { + return this.isSame(input, units) || this.isAfter(input,units); + } + + function isSameOrBefore (input, units) { + return this.isSame(input, units) || this.isBefore(input,units); + } + + function diff (input, units, asFloat) { + var that, + zoneDelta, + delta, output; + + if (!this.isValid()) { + return NaN; + } + + that = cloneWithOffset(input, this); + + if (!that.isValid()) { + return NaN; + } + + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4; + + units = normalizeUnits(units); + + if (units === 'year' || units === 'month' || units === 'quarter') { + output = monthDiff(this, that); + if (units === 'quarter') { + output = output / 3; + } else if (units === 'year') { + output = output / 12; + } + } else { + delta = this - that; + output = units === 'second' ? delta / 1e3 : // 1000 + units === 'minute' ? delta / 6e4 : // 1000 * 60 + units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + delta; + } + return asFloat ? output : absFloor(output); + } + + function monthDiff (a, b) { + // difference in months + var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, adjust; + + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } + + return -(wholeMonthDiff + adjust); + } + + utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; + + function toString () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + } + + function moment_format__toISOString () { + var m = this.clone().utc(); + if (0 < m.year() && m.year() <= 9999) { + if (isFunction(Date.prototype.toISOString)) { + // native implementation is ~50x faster, use it when we can + return this.toDate().toISOString(); + } else { + return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } else { + return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } + + function format (inputString) { + var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); + return this.localeData().postformat(output); + } + + function from (time, withoutSuffix) { + if (this.isValid() && + ((isMoment(time) && time.isValid()) || + local__createLocal(time).isValid())) { + return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } + } + + function fromNow (withoutSuffix) { + return this.from(local__createLocal(), withoutSuffix); + } + + function to (time, withoutSuffix) { + if (this.isValid() && + ((isMoment(time) && time.isValid()) || + local__createLocal(time).isValid())) { + return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } + } + + function toNow (withoutSuffix) { + return this.to(local__createLocal(), withoutSuffix); + } + + // If passed a locale key, it will set the locale for this + // instance. Otherwise, it will return the locale configuration + // variables for this instance. + function locale (key) { + var newLocaleData; + + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = locale_locales__getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + } + + var lang = deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } + ); + + function localeData () { + return this._locale; + } + + function startOf (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'quarter': + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + } + + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } + if (units === 'isoWeek') { + this.isoWeekday(1); + } + + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } + + return this; + } + + function endOf (units) { + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond') { + return this; + } + return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); + } + + function to_type__valueOf () { + return +this._d - ((this._offset || 0) * 60000); + } + + function unix () { + return Math.floor(+this / 1000); + } + + function toDate () { + return this._offset ? new Date(+this) : this._d; + } + + function toArray () { + var m = this; + return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; + } + + function toObject () { + var m = this; + return { + years: m.year(), + months: m.month(), + date: m.date(), + hours: m.hours(), + minutes: m.minutes(), + seconds: m.seconds(), + milliseconds: m.milliseconds() + }; + } + + function toJSON () { + // JSON.stringify(new Date(NaN)) === 'null' + return this.isValid() ? this.toISOString() : 'null'; + } + + function moment_valid__isValid () { + return valid__isValid(this); + } + + function parsingFlags () { + return extend({}, getParsingFlags(this)); + } + + function invalidAt () { + return getParsingFlags(this).overflow; + } + + function creationData() { + return { + input: this._i, + format: this._f, + locale: this._locale, + isUTC: this._isUTC, + strict: this._strict + }; + } + + // FORMATTING + + addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; + }); + + addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; + }); + + function addWeekYearFormatToken (token, getter) { + addFormatToken(0, [token, token.length], 0, getter); + } + + addWeekYearFormatToken('gggg', 'weekYear'); + addWeekYearFormatToken('ggggg', 'weekYear'); + addWeekYearFormatToken('GGGG', 'isoWeekYear'); + addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + + // ALIASES + + addUnitAlias('weekYear', 'gg'); + addUnitAlias('isoWeekYear', 'GG'); + + // PARSING + + addRegexToken('G', matchSigned); + addRegexToken('g', matchSigned); + addRegexToken('GG', match1to2, match2); + addRegexToken('gg', match1to2, match2); + addRegexToken('GGGG', match1to4, match4); + addRegexToken('gggg', match1to4, match4); + addRegexToken('GGGGG', match1to6, match6); + addRegexToken('ggggg', match1to6, match6); + + addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { + week[token.substr(0, 2)] = toInt(input); + }); + + addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + week[token] = utils_hooks__hooks.parseTwoDigitYear(input); + }); + + // MOMENTS + + function getSetWeekYear (input) { + return getSetWeekYearHelper.call(this, + input, + this.week(), + this.weekday(), + this.localeData()._week.dow, + this.localeData()._week.doy); + } + + function getSetISOWeekYear (input) { + return getSetWeekYearHelper.call(this, + input, this.isoWeek(), this.isoWeekday(), 1, 4); + } + + function getISOWeeksInYear () { + return weeksInYear(this.year(), 1, 4); + } + + function getWeeksInYear () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + } + + function getSetWeekYearHelper(input, week, weekday, dow, doy) { + var weeksTarget; + if (input == null) { + return weekOfYear(this, dow, doy).year; + } else { + weeksTarget = weeksInYear(input, dow, doy); + if (week > weeksTarget) { + week = weeksTarget; + } + return setWeekAll.call(this, input, week, weekday, dow, doy); + } + } + + function setWeekAll(weekYear, week, weekday, dow, doy) { + var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), + date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); + + // console.log("got", weekYear, week, weekday, "set", date.toISOString()); + this.year(date.getUTCFullYear()); + this.month(date.getUTCMonth()); + this.date(date.getUTCDate()); + return this; + } + + // FORMATTING + + addFormatToken('Q', 0, 'Qo', 'quarter'); + + // ALIASES + + addUnitAlias('quarter', 'Q'); + + // PARSING + + addRegexToken('Q', match1); + addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; + }); + + // MOMENTS + + function getSetQuarter (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + } + + // FORMATTING + + addFormatToken('w', ['ww', 2], 'wo', 'week'); + addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); + + // ALIASES + + addUnitAlias('week', 'w'); + addUnitAlias('isoWeek', 'W'); + + // PARSING + + addRegexToken('w', match1to2); + addRegexToken('ww', match1to2, match2); + addRegexToken('W', match1to2); + addRegexToken('WW', match1to2, match2); + + addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { + week[token.substr(0, 1)] = toInt(input); + }); + + // HELPERS + + // LOCALES + + function localeWeek (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + } + + var defaultLocaleWeek = { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + }; + + function localeFirstDayOfWeek () { + return this._week.dow; + } + + function localeFirstDayOfYear () { + return this._week.doy; + } + + // MOMENTS + + function getSetWeek (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); + } + + function getSetISOWeek (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); + } + + // FORMATTING + + addFormatToken('D', ['DD', 2], 'Do', 'date'); + + // ALIASES + + addUnitAlias('date', 'D'); + + // PARSING + + addRegexToken('D', match1to2); + addRegexToken('DD', match1to2, match2); + addRegexToken('Do', function (isStrict, locale) { + return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; + }); + + addParseToken(['D', 'DD'], DATE); + addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0], 10); + }); + + // MOMENTS + + var getSetDayOfMonth = makeGetSet('Date', true); + + // FORMATTING + + addFormatToken('d', 0, 'do', 'day'); + + addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); + }); + + addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); + }); + + addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); + }); + + addFormatToken('e', 0, 0, 'weekday'); + addFormatToken('E', 0, 0, 'isoWeekday'); + + // ALIASES + + addUnitAlias('day', 'd'); + addUnitAlias('weekday', 'e'); + addUnitAlias('isoWeekday', 'E'); + + // PARSING + + addRegexToken('d', match1to2); + addRegexToken('e', match1to2); + addRegexToken('E', match1to2); + addRegexToken('dd', matchWord); + addRegexToken('ddd', matchWord); + addRegexToken('dddd', matchWord); + + addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) { + var weekday = config._locale.weekdaysParse(input, token, config._strict); + // if we didn't get a weekday name, mark the date as invalid + if (weekday != null) { + week.d = weekday; + } else { + getParsingFlags(config).invalidWeekday = input; + } + }); + + addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); + }); + + // HELPERS + + function parseWeekday(input, locale) { + if (typeof input !== 'string') { + return input; + } + + if (!isNaN(input)) { + return parseInt(input, 10); + } + + input = locale.weekdaysParse(input); + if (typeof input === 'number') { + return input; + } + + return null; + } + + // LOCALES + + var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); + function localeWeekdays (m, format) { + return isArray(this._weekdays) ? this._weekdays[m.day()] : + this._weekdays[this._weekdays.isFormat.test(format) ? 'format' : 'standalone'][m.day()]; + } + + var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); + function localeWeekdaysShort (m) { + return this._weekdaysShort[m.day()]; + } + + var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); + function localeWeekdaysMin (m) { + return this._weekdaysMin[m.day()]; + } + + function localeWeekdaysParse (weekdayName, format, strict) { + var i, mom, regex; + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + this._minWeekdaysParse = []; + this._shortWeekdaysParse = []; + this._fullWeekdaysParse = []; + } + + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + + mom = local__createLocal([2000, 1]).day(i); + if (strict && !this._fullWeekdaysParse[i]) { + this._fullWeekdaysParse[i] = new RegExp('^' + this.weekdays(mom, '').replace('.', '\.?') + '$', 'i'); + this._shortWeekdaysParse[i] = new RegExp('^' + this.weekdaysShort(mom, '').replace('.', '\.?') + '$', 'i'); + this._minWeekdaysParse[i] = new RegExp('^' + this.weekdaysMin(mom, '').replace('.', '\.?') + '$', 'i'); + } + if (!this._weekdaysParse[i]) { + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (strict && format === 'dddd' && this._fullWeekdaysParse[i].test(weekdayName)) { + return i; + } else if (strict && format === 'ddd' && this._shortWeekdaysParse[i].test(weekdayName)) { + return i; + } else if (strict && format === 'dd' && this._minWeekdaysParse[i].test(weekdayName)) { + return i; + } else if (!strict && this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } + } + + // MOMENTS + + function getSetDayOfWeek (input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; + } + } + + function getSetLocaleDayOfWeek (input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + } + + function getSetISODayOfWeek (input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + } + + // FORMATTING + + addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); + + // ALIASES + + addUnitAlias('dayOfYear', 'DDD'); + + // PARSING + + addRegexToken('DDD', match1to3); + addRegexToken('DDDD', match3); + addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); + }); + + // HELPERS + + // MOMENTS + + function getSetDayOfYear (input) { + var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); + } + + // FORMATTING + + function hFormat() { + return this.hours() % 12 || 12; + } + + addFormatToken('H', ['HH', 2], 0, 'hour'); + addFormatToken('h', ['hh', 2], 0, hFormat); + + addFormatToken('hmm', 0, 0, function () { + return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2); + }); + + addFormatToken('hmmss', 0, 0, function () { + return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2); + }); + + addFormatToken('Hmm', 0, 0, function () { + return '' + this.hours() + zeroFill(this.minutes(), 2); + }); + + addFormatToken('Hmmss', 0, 0, function () { + return '' + this.hours() + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2); + }); + + function meridiem (token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); + }); + } + + meridiem('a', true); + meridiem('A', false); + + // ALIASES + + addUnitAlias('hour', 'h'); + + // PARSING + + function matchMeridiem (isStrict, locale) { + return locale._meridiemParse; + } + + addRegexToken('a', matchMeridiem); + addRegexToken('A', matchMeridiem); + addRegexToken('H', match1to2); + addRegexToken('h', match1to2); + addRegexToken('HH', match1to2, match2); + addRegexToken('hh', match1to2, match2); + + addRegexToken('hmm', match3to4); + addRegexToken('hmmss', match5to6); + addRegexToken('Hmm', match3to4); + addRegexToken('Hmmss', match5to6); + + addParseToken(['H', 'HH'], HOUR); + addParseToken(['a', 'A'], function (input, array, config) { + config._isPm = config._locale.isPM(input); + config._meridiem = input; + }); + addParseToken(['h', 'hh'], function (input, array, config) { + array[HOUR] = toInt(input); + getParsingFlags(config).bigHour = true; + }); + addParseToken('hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); + getParsingFlags(config).bigHour = true; + }); + addParseToken('hmmss', function (input, array, config) { + var pos1 = input.length - 4; + var pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); + getParsingFlags(config).bigHour = true; + }); + addParseToken('Hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); + }); + addParseToken('Hmmss', function (input, array, config) { + var pos1 = input.length - 4; + var pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); + }); + + // LOCALES + + function localeIsPM (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); + } + + var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; + function localeMeridiem (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + } + + + // MOMENTS + // Setting the hour should keep the time, because the user explicitly // specified which hour he wants. So trying to maintain the same hour (in // a new timezone) makes sense. Adding/subtracting hours does not follow // this rule. - moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true); - // moment.fn.month is defined separately - moment.fn.date = makeAccessor('Date', true); - moment.fn.dates = deprecate('dates accessor is deprecated. Use date instead.', makeAccessor('Date', true)); - moment.fn.year = makeAccessor('FullYear', true); - moment.fn.years = deprecate('years accessor is deprecated. Use year instead.', makeAccessor('FullYear', true)); + var getSetHour = makeGetSet('Hours', true); - // add plural methods - moment.fn.days = moment.fn.day; - moment.fn.months = moment.fn.month; - moment.fn.weeks = moment.fn.week; - moment.fn.isoWeeks = moment.fn.isoWeek; - moment.fn.quarters = moment.fn.quarter; + // FORMATTING - // add aliased format methods - moment.fn.toJSON = moment.fn.toISOString; + addFormatToken('m', ['mm', 2], 0, 'minute'); - /************************************ - Duration Prototype - ************************************/ + // ALIASES + addUnitAlias('minute', 'm'); - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; - } + // PARSING - function yearsToDays (years) { - // years * 365 + absRound(years / 4) - - // absRound(years / 100) + absRound(years / 400); - return years * 146097 / 400; - } + addRegexToken('m', match1to2); + addRegexToken('mm', match1to2, match2); + addParseToken(['m', 'mm'], MINUTE); - extend(moment.duration.fn = Duration.prototype, { + // MOMENTS - _bubble : function () { - var milliseconds = this._milliseconds, - days = this._days, - months = this._months, - data = this._data, - seconds, minutes, hours, years = 0; + var getSetMinute = makeGetSet('Minutes', false); - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; + // FORMATTING - seconds = absRound(milliseconds / 1000); - data.seconds = seconds % 60; + addFormatToken('s', ['ss', 2], 0, 'second'); - minutes = absRound(seconds / 60); - data.minutes = minutes % 60; + // ALIASES - hours = absRound(minutes / 60); - data.hours = hours % 24; + addUnitAlias('second', 's'); - days += absRound(hours / 24); + // PARSING - // Accurately convert days to years, assume start from year 0. - years = absRound(daysToYears(days)); - days -= absRound(yearsToDays(years)); + addRegexToken('s', match1to2); + addRegexToken('ss', match1to2, match2); + addParseToken(['s', 'ss'], SECOND); - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absRound(days / 30); - days %= 30; + // MOMENTS - // 12 months -> 1 year - years += absRound(months / 12); - months %= 12; + var getSetSecond = makeGetSet('Seconds', false); - data.days = days; - data.months = months; - data.years = years; - }, + // FORMATTING - abs : function () { - this._milliseconds = Math.abs(this._milliseconds); - this._days = Math.abs(this._days); - this._months = Math.abs(this._months); - - this._data.milliseconds = Math.abs(this._data.milliseconds); - this._data.seconds = Math.abs(this._data.seconds); - this._data.minutes = Math.abs(this._data.minutes); - this._data.hours = Math.abs(this._data.hours); - this._data.months = Math.abs(this._data.months); - this._data.years = Math.abs(this._data.years); - - return this; - }, - - weeks : function () { - return absRound(this.days() / 7); - }, - - valueOf : function () { - return this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6; - }, - - humanize : function (withSuffix) { - var output = relativeTime(this, !withSuffix, this.localeData()); - - if (withSuffix) { - output = this.localeData().pastFuture(+this, output); - } - - return this.localeData().postformat(output); - }, - - add : function (input, val) { - // supports only 2.0-style add(1, 's') or add(moment) - var dur = moment.duration(input, val); - - this._milliseconds += dur._milliseconds; - this._days += dur._days; - this._months += dur._months; - - this._bubble(); - - return this; - }, - - subtract : function (input, val) { - var dur = moment.duration(input, val); - - this._milliseconds -= dur._milliseconds; - this._days -= dur._days; - this._months -= dur._months; - - this._bubble(); - - return this; - }, - - get : function (units) { - units = normalizeUnits(units); - return this[units.toLowerCase() + 's'](); - }, - - as : function (units) { - var days, months; - units = normalizeUnits(units); - - days = this._days + this._milliseconds / 864e5; - if (units === 'month' || units === 'year') { - months = this._months + daysToYears(days) * 12; - return units === 'month' ? months : months / 12; - } else { - days += yearsToDays(this._months / 12); - switch (units) { - case 'week': return days / 7; - case 'day': return days; - case 'hour': return days * 24; - case 'minute': return days * 24 * 60; - case 'second': return days * 24 * 60 * 60; - case 'millisecond': return days * 24 * 60 * 60 * 1000; - default: throw new Error('Unknown unit ' + units); - } - } - }, - - lang : moment.fn.lang, - locale : moment.fn.locale, - - toIsoString : deprecate( - "toIsoString() is deprecated. Please use toISOString() instead " + - "(notice the capitals)", - function () { - return this.toISOString(); - } - ), - - toISOString : function () { - // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - var years = Math.abs(this.years()), - months = Math.abs(this.months()), - days = Math.abs(this.days()), - hours = Math.abs(this.hours()), - minutes = Math.abs(this.minutes()), - seconds = Math.abs(this.seconds() + this.milliseconds() / 1000); - - if (!this.asSeconds()) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } - - return (this.asSeconds() < 0 ? '-' : '') + - 'P' + - (years ? years + 'Y' : '') + - (months ? months + 'M' : '') + - (days ? days + 'D' : '') + - ((hours || minutes || seconds) ? 'T' : '') + - (hours ? hours + 'H' : '') + - (minutes ? minutes + 'M' : '') + - (seconds ? seconds + 'S' : ''); - }, - - localeData : function () { - return this._locale; - } + addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); }); - function makeDurationGetter(name) { - moment.duration.fn[name] = function () { - return this._data[name]; - }; + addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); + }); + + addFormatToken(0, ['SSS', 3], 0, 'millisecond'); + addFormatToken(0, ['SSSS', 4], 0, function () { + return this.millisecond() * 10; + }); + addFormatToken(0, ['SSSSS', 5], 0, function () { + return this.millisecond() * 100; + }); + addFormatToken(0, ['SSSSSS', 6], 0, function () { + return this.millisecond() * 1000; + }); + addFormatToken(0, ['SSSSSSS', 7], 0, function () { + return this.millisecond() * 10000; + }); + addFormatToken(0, ['SSSSSSSS', 8], 0, function () { + return this.millisecond() * 100000; + }); + addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { + return this.millisecond() * 1000000; + }); + + + // ALIASES + + addUnitAlias('millisecond', 'ms'); + + // PARSING + + addRegexToken('S', match1to3, match1); + addRegexToken('SS', match1to3, match2); + addRegexToken('SSS', match1to3, match3); + + var token; + for (token = 'SSSS'; token.length <= 9; token += 'S') { + addRegexToken(token, matchUnsigned); } - for (i in unitMillisecondFactors) { - if (unitMillisecondFactors.hasOwnProperty(i)) { - makeDurationGetter(i.toLowerCase()); + function parseMs(input, array) { + array[MILLISECOND] = toInt(('0.' + input) * 1000); + } + + for (token = 'S'; token.length <= 9; token += 'S') { + addParseToken(token, parseMs); + } + // MOMENTS + + var getSetMillisecond = makeGetSet('Milliseconds', false); + + // FORMATTING + + addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('zz', 0, 0, 'zoneName'); + + // MOMENTS + + function getZoneAbbr () { + return this._isUTC ? 'UTC' : ''; + } + + function getZoneName () { + return this._isUTC ? 'Coordinated Universal Time' : ''; + } + + var momentPrototype__proto = Moment.prototype; + + momentPrototype__proto.add = add_subtract__add; + momentPrototype__proto.calendar = moment_calendar__calendar; + momentPrototype__proto.clone = clone; + momentPrototype__proto.diff = diff; + momentPrototype__proto.endOf = endOf; + momentPrototype__proto.format = format; + momentPrototype__proto.from = from; + momentPrototype__proto.fromNow = fromNow; + momentPrototype__proto.to = to; + momentPrototype__proto.toNow = toNow; + momentPrototype__proto.get = getSet; + momentPrototype__proto.invalidAt = invalidAt; + momentPrototype__proto.isAfter = isAfter; + momentPrototype__proto.isBefore = isBefore; + momentPrototype__proto.isBetween = isBetween; + momentPrototype__proto.isSame = isSame; + momentPrototype__proto.isSameOrAfter = isSameOrAfter; + momentPrototype__proto.isSameOrBefore = isSameOrBefore; + momentPrototype__proto.isValid = moment_valid__isValid; + momentPrototype__proto.lang = lang; + momentPrototype__proto.locale = locale; + momentPrototype__proto.localeData = localeData; + momentPrototype__proto.max = prototypeMax; + momentPrototype__proto.min = prototypeMin; + momentPrototype__proto.parsingFlags = parsingFlags; + momentPrototype__proto.set = getSet; + momentPrototype__proto.startOf = startOf; + momentPrototype__proto.subtract = add_subtract__subtract; + momentPrototype__proto.toArray = toArray; + momentPrototype__proto.toObject = toObject; + momentPrototype__proto.toDate = toDate; + momentPrototype__proto.toISOString = moment_format__toISOString; + momentPrototype__proto.toJSON = toJSON; + momentPrototype__proto.toString = toString; + momentPrototype__proto.unix = unix; + momentPrototype__proto.valueOf = to_type__valueOf; + momentPrototype__proto.creationData = creationData; + + // Year + momentPrototype__proto.year = getSetYear; + momentPrototype__proto.isLeapYear = getIsLeapYear; + + // Week Year + momentPrototype__proto.weekYear = getSetWeekYear; + momentPrototype__proto.isoWeekYear = getSetISOWeekYear; + + // Quarter + momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; + + // Month + momentPrototype__proto.month = getSetMonth; + momentPrototype__proto.daysInMonth = getDaysInMonth; + + // Week + momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; + momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; + momentPrototype__proto.weeksInYear = getWeeksInYear; + momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; + + // Day + momentPrototype__proto.date = getSetDayOfMonth; + momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; + momentPrototype__proto.weekday = getSetLocaleDayOfWeek; + momentPrototype__proto.isoWeekday = getSetISODayOfWeek; + momentPrototype__proto.dayOfYear = getSetDayOfYear; + + // Hour + momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; + + // Minute + momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; + + // Second + momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; + + // Millisecond + momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; + + // Offset + momentPrototype__proto.utcOffset = getSetOffset; + momentPrototype__proto.utc = setOffsetToUTC; + momentPrototype__proto.local = setOffsetToLocal; + momentPrototype__proto.parseZone = setOffsetToParsedOffset; + momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; + momentPrototype__proto.isDST = isDaylightSavingTime; + momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; + momentPrototype__proto.isLocal = isLocal; + momentPrototype__proto.isUtcOffset = isUtcOffset; + momentPrototype__proto.isUtc = isUtc; + momentPrototype__proto.isUTC = isUtc; + + // Timezone + momentPrototype__proto.zoneAbbr = getZoneAbbr; + momentPrototype__proto.zoneName = getZoneName; + + // Deprecations + momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); + momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); + momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); + momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); + + var momentPrototype = momentPrototype__proto; + + function moment__createUnix (input) { + return local__createLocal(input * 1000); + } + + function moment__createInZone () { + return local__createLocal.apply(null, arguments).parseZone(); + } + + var defaultCalendar = { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }; + + function locale_calendar__calendar (key, mom, now) { + var output = this._calendar[key]; + return isFunction(output) ? output.call(mom, now) : output; + } + + var defaultLongDateFormat = { + LTS : 'h:mm:ss A', + LT : 'h:mm A', + L : 'MM/DD/YYYY', + LL : 'MMMM D, YYYY', + LLL : 'MMMM D, YYYY h:mm A', + LLLL : 'dddd, MMMM D, YYYY h:mm A' + }; + + function longDateFormat (key) { + var format = this._longDateFormat[key], + formatUpper = this._longDateFormat[key.toUpperCase()]; + + if (format || !formatUpper) { + return format; } + + this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + + return this._longDateFormat[key]; } - moment.duration.fn.asMilliseconds = function () { - return this.as('ms'); - }; - moment.duration.fn.asSeconds = function () { - return this.as('s'); - }; - moment.duration.fn.asMinutes = function () { - return this.as('m'); - }; - moment.duration.fn.asHours = function () { - return this.as('h'); - }; - moment.duration.fn.asDays = function () { - return this.as('d'); - }; - moment.duration.fn.asWeeks = function () { - return this.as('weeks'); - }; - moment.duration.fn.asMonths = function () { - return this.as('M'); - }; - moment.duration.fn.asYears = function () { - return this.as('y'); + var defaultInvalidDate = 'Invalid date'; + + function invalidDate () { + return this._invalidDate; + } + + var defaultOrdinal = '%d'; + var defaultOrdinalParse = /\d{1,2}/; + + function ordinal (number) { + return this._ordinal.replace('%d', number); + } + + function preParsePostFormat (string) { + return string; + } + + var defaultRelativeTime = { + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' }; - /************************************ - Default Locale - ************************************/ + function relative__relativeTime (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (isFunction(output)) ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); + } + function pastFuture (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return isFunction(format) ? format(output) : format.replace(/%s/i, output); + } - // Set default locale, other locale will inherit from English. - moment.locale('en', { + function locale_set__set (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (isFunction(prop)) { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _ordinalParseLenient. + this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); + } + + var prototype__proto = Locale.prototype; + + prototype__proto._calendar = defaultCalendar; + prototype__proto.calendar = locale_calendar__calendar; + prototype__proto._longDateFormat = defaultLongDateFormat; + prototype__proto.longDateFormat = longDateFormat; + prototype__proto._invalidDate = defaultInvalidDate; + prototype__proto.invalidDate = invalidDate; + prototype__proto._ordinal = defaultOrdinal; + prototype__proto.ordinal = ordinal; + prototype__proto._ordinalParse = defaultOrdinalParse; + prototype__proto.preparse = preParsePostFormat; + prototype__proto.postformat = preParsePostFormat; + prototype__proto._relativeTime = defaultRelativeTime; + prototype__proto.relativeTime = relative__relativeTime; + prototype__proto.pastFuture = pastFuture; + prototype__proto.set = locale_set__set; + + // Month + prototype__proto.months = localeMonths; + prototype__proto._months = defaultLocaleMonths; + prototype__proto.monthsShort = localeMonthsShort; + prototype__proto._monthsShort = defaultLocaleMonthsShort; + prototype__proto.monthsParse = localeMonthsParse; + prototype__proto._monthsRegex = defaultMonthsRegex; + prototype__proto.monthsRegex = monthsRegex; + prototype__proto._monthsShortRegex = defaultMonthsShortRegex; + prototype__proto.monthsShortRegex = monthsShortRegex; + + // Week + prototype__proto.week = localeWeek; + prototype__proto._week = defaultLocaleWeek; + prototype__proto.firstDayOfYear = localeFirstDayOfYear; + prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; + + // Day of Week + prototype__proto.weekdays = localeWeekdays; + prototype__proto._weekdays = defaultLocaleWeekdays; + prototype__proto.weekdaysMin = localeWeekdaysMin; + prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; + prototype__proto.weekdaysShort = localeWeekdaysShort; + prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; + prototype__proto.weekdaysParse = localeWeekdaysParse; + + // Hours + prototype__proto.isPM = localeIsPM; + prototype__proto._meridiemParse = defaultLocaleMeridiemParse; + prototype__proto.meridiem = localeMeridiem; + + function lists__get (format, index, field, setter) { + var locale = locale_locales__getLocale(); + var utc = create_utc__createUTC().set(setter, index); + return locale[field](utc, format); + } + + function list (format, index, field, count, setter) { + if (typeof format === 'number') { + index = format; + format = undefined; + } + + format = format || ''; + + if (index != null) { + return lists__get(format, index, field, setter); + } + + var i; + var out = []; + for (i = 0; i < count; i++) { + out[i] = lists__get(format, i, field, setter); + } + return out; + } + + function lists__listMonths (format, index) { + return list(format, index, 'months', 12, 'month'); + } + + function lists__listMonthsShort (format, index) { + return list(format, index, 'monthsShort', 12, 'month'); + } + + function lists__listWeekdays (format, index) { + return list(format, index, 'weekdays', 7, 'day'); + } + + function lists__listWeekdaysShort (format, index) { + return list(format, index, 'weekdaysShort', 7, 'day'); + } + + function lists__listWeekdaysMin (format, index) { + return list(format, index, 'weekdaysMin', 7, 'day'); + } + + locale_locales__getSetGlobalLocale('en', { + ordinalParse: /\d{1,2}(th|st|nd|rd)/, ordinal : function (number) { var b = number % 10, output = (toInt(number % 100 / 10) === 1) ? 'th' : @@ -2766,43 +3215,392 @@ } }); - /* EMBED_LOCALES */ + // Side effect imports + utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); + utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); - /************************************ - Exposing Moment - ************************************/ + var mathAbs = Math.abs; - function makeGlobal(shouldDeprecate) { - /*global ender:false */ - if (typeof ender !== 'undefined') { - return; - } - oldGlobalMoment = globalScope.moment; - if (shouldDeprecate) { - globalScope.moment = deprecate( - 'Accessing Moment through the global scope is ' + - 'deprecated, and will be removed in an upcoming ' + - 'release.', - moment); + function duration_abs__abs () { + var data = this._data; + + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); + + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); + + return this; + } + + function duration_add_subtract__addSubtract (duration, input, value, direction) { + var other = create__createDuration(input, value); + + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; + + return duration._bubble(); + } + + // supports only 2.0-style add(1, 's') or add(duration) + function duration_add_subtract__add (input, value) { + return duration_add_subtract__addSubtract(this, input, value, 1); + } + + // supports only 2.0-style subtract(1, 's') or subtract(duration) + function duration_add_subtract__subtract (input, value) { + return duration_add_subtract__addSubtract(this, input, value, -1); + } + + function absCeil (number) { + if (number < 0) { + return Math.floor(number); } else { - globalScope.moment = moment; + return Math.ceil(number); } } - // CommonJS module is defined - if (hasModule) { - module.exports = moment; - } else if (typeof define === 'function' && define.amd) { - define('moment', function (require, exports, module) { - if (module.config && module.config() && module.config().noGlobal === true) { - // release the global variable - globalScope.moment = oldGlobalMoment; - } + function bubble () { + var milliseconds = this._milliseconds; + var days = this._days; + var months = this._months; + var data = this._data; + var seconds, minutes, hours, years, monthsFromDays; - return moment; - }); - makeGlobal(true); - } else { - makeGlobal(); + // if we have a mix of positive and negative values, bubble down first + // check: https://github.com/moment/moment/issues/2166 + if (!((milliseconds >= 0 && days >= 0 && months >= 0) || + (milliseconds <= 0 && days <= 0 && months <= 0))) { + milliseconds += absCeil(monthsToDays(months) + days) * 864e5; + days = 0; + months = 0; + } + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; + + hours = absFloor(minutes / 60); + data.hours = hours % 24; + + days += absFloor(hours / 24); + + // convert days to months + monthsFromDays = absFloor(daysToMonths(days)); + months += monthsFromDays; + days -= absCeil(monthsToDays(monthsFromDays)); + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + data.days = days; + data.months = months; + data.years = years; + + return this; } -}).call(this); + + function daysToMonths (days) { + // 400 years have 146097 days (taking into account leap year rules) + // 400 years have 12 months === 4800 + return days * 4800 / 146097; + } + + function monthsToDays (months) { + // the reverse of daysToMonths + return months * 146097 / 4800; + } + + function as (units) { + var days; + var months; + var milliseconds = this._milliseconds; + + units = normalizeUnits(units); + + if (units === 'month' || units === 'year') { + days = this._days + milliseconds / 864e5; + months = this._months + daysToMonths(days); + return units === 'month' ? months : months / 12; + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(monthsToDays(this._months)); + switch (units) { + case 'week' : return days / 7 + milliseconds / 6048e5; + case 'day' : return days + milliseconds / 864e5; + case 'hour' : return days * 24 + milliseconds / 36e5; + case 'minute' : return days * 1440 + milliseconds / 6e4; + case 'second' : return days * 86400 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': return Math.floor(days * 864e5) + milliseconds; + default: throw new Error('Unknown unit ' + units); + } + } + } + + // TODO: Use this.as('ms')? + function duration_as__valueOf () { + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); + } + + function makeAs (alias) { + return function () { + return this.as(alias); + }; + } + + var asMilliseconds = makeAs('ms'); + var asSeconds = makeAs('s'); + var asMinutes = makeAs('m'); + var asHours = makeAs('h'); + var asDays = makeAs('d'); + var asWeeks = makeAs('w'); + var asMonths = makeAs('M'); + var asYears = makeAs('y'); + + function duration_get__get (units) { + units = normalizeUnits(units); + return this[units + 's'](); + } + + function makeGetter(name) { + return function () { + return this._data[name]; + }; + } + + var milliseconds = makeGetter('milliseconds'); + var seconds = makeGetter('seconds'); + var minutes = makeGetter('minutes'); + var hours = makeGetter('hours'); + var days = makeGetter('days'); + var months = makeGetter('months'); + var years = makeGetter('years'); + + function weeks () { + return absFloor(this.days() / 7); + } + + var round = Math.round; + var thresholds = { + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month + M: 11 // months to year + }; + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } + + function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { + var duration = create__createDuration(posNegDuration).abs(); + var seconds = round(duration.as('s')); + var minutes = round(duration.as('m')); + var hours = round(duration.as('h')); + var days = round(duration.as('d')); + var months = round(duration.as('M')); + var years = round(duration.as('y')); + + var a = seconds < thresholds.s && ['s', seconds] || + minutes <= 1 && ['m'] || + minutes < thresholds.m && ['mm', minutes] || + hours <= 1 && ['h'] || + hours < thresholds.h && ['hh', hours] || + days <= 1 && ['d'] || + days < thresholds.d && ['dd', days] || + months <= 1 && ['M'] || + months < thresholds.M && ['MM', months] || + years <= 1 && ['y'] || ['yy', years]; + + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); + } + + // This function allows you to set a threshold for relative time strings + function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { + if (thresholds[threshold] === undefined) { + return false; + } + if (limit === undefined) { + return thresholds[threshold]; + } + thresholds[threshold] = limit; + return true; + } + + function humanize (withSuffix) { + var locale = this.localeData(); + var output = duration_humanize__relativeTime(this, !withSuffix, locale); + + if (withSuffix) { + output = locale.pastFuture(+this, output); + } + + return locale.postformat(output); + } + + var iso_string__abs = Math.abs; + + function iso_string__toISOString() { + // for ISO strings we do not use the normal bubbling rules: + // * milliseconds bubble up until they become hours + // * days do not bubble at all + // * months bubble up until they become years + // This is because there is no context-free conversion between hours and days + // (think of clock changes) + // and also not between days and months (28-31 days per month) + var seconds = iso_string__abs(this._milliseconds) / 1000; + var days = iso_string__abs(this._days); + var months = iso_string__abs(this._months); + var minutes, hours, years; + + // 3600 seconds -> 60 minutes -> 1 hour + minutes = absFloor(seconds / 60); + hours = absFloor(minutes / 60); + seconds %= 60; + minutes %= 60; + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var Y = years; + var M = months; + var D = days; + var h = hours; + var m = minutes; + var s = seconds; + var total = this.asSeconds(); + + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + return (total < 0 ? '-' : '') + + 'P' + + (Y ? Y + 'Y' : '') + + (M ? M + 'M' : '') + + (D ? D + 'D' : '') + + ((h || m || s) ? 'T' : '') + + (h ? h + 'H' : '') + + (m ? m + 'M' : '') + + (s ? s + 'S' : ''); + } + + var duration_prototype__proto = Duration.prototype; + + duration_prototype__proto.abs = duration_abs__abs; + duration_prototype__proto.add = duration_add_subtract__add; + duration_prototype__proto.subtract = duration_add_subtract__subtract; + duration_prototype__proto.as = as; + duration_prototype__proto.asMilliseconds = asMilliseconds; + duration_prototype__proto.asSeconds = asSeconds; + duration_prototype__proto.asMinutes = asMinutes; + duration_prototype__proto.asHours = asHours; + duration_prototype__proto.asDays = asDays; + duration_prototype__proto.asWeeks = asWeeks; + duration_prototype__proto.asMonths = asMonths; + duration_prototype__proto.asYears = asYears; + duration_prototype__proto.valueOf = duration_as__valueOf; + duration_prototype__proto._bubble = bubble; + duration_prototype__proto.get = duration_get__get; + duration_prototype__proto.milliseconds = milliseconds; + duration_prototype__proto.seconds = seconds; + duration_prototype__proto.minutes = minutes; + duration_prototype__proto.hours = hours; + duration_prototype__proto.days = days; + duration_prototype__proto.weeks = weeks; + duration_prototype__proto.months = months; + duration_prototype__proto.years = years; + duration_prototype__proto.humanize = humanize; + duration_prototype__proto.toISOString = iso_string__toISOString; + duration_prototype__proto.toString = iso_string__toISOString; + duration_prototype__proto.toJSON = iso_string__toISOString; + duration_prototype__proto.locale = locale; + duration_prototype__proto.localeData = localeData; + + // Deprecations + duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); + duration_prototype__proto.lang = lang; + + // Side effect imports + + // FORMATTING + + addFormatToken('X', 0, 0, 'unix'); + addFormatToken('x', 0, 0, 'valueOf'); + + // PARSING + + addRegexToken('x', matchSigned); + addRegexToken('X', matchTimestamp); + addParseToken('X', function (input, array, config) { + config._d = new Date(parseFloat(input, 10) * 1000); + }); + addParseToken('x', function (input, array, config) { + config._d = new Date(toInt(input)); + }); + + // Side effect imports + + + utils_hooks__hooks.version = '2.11.2'; + + setHookCallback(local__createLocal); + + utils_hooks__hooks.fn = momentPrototype; + utils_hooks__hooks.min = min; + utils_hooks__hooks.max = max; + utils_hooks__hooks.now = now; + utils_hooks__hooks.utc = create_utc__createUTC; + utils_hooks__hooks.unix = moment__createUnix; + utils_hooks__hooks.months = lists__listMonths; + utils_hooks__hooks.isDate = isDate; + utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; + utils_hooks__hooks.invalid = valid__createInvalid; + utils_hooks__hooks.duration = create__createDuration; + utils_hooks__hooks.isMoment = isMoment; + utils_hooks__hooks.weekdays = lists__listWeekdays; + utils_hooks__hooks.parseZone = moment__createInZone; + utils_hooks__hooks.localeData = locale_locales__getLocale; + utils_hooks__hooks.isDuration = isDuration; + utils_hooks__hooks.monthsShort = lists__listMonthsShort; + utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; + utils_hooks__hooks.defineLocale = defineLocale; + utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; + utils_hooks__hooks.normalizeUnits = normalizeUnits; + utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; + utils_hooks__hooks.prototype = momentPrototype; + + var _moment = utils_hooks__hooks; + + return _moment; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/af.js b/lib/javascripts/moment_locale/af.js index 2777e58e77..1a96bf4735 100644 --- a/lib/javascripts/moment_locale/af.js +++ b/lib/javascripts/moment_locale/af.js @@ -1,22 +1,25 @@ -// moment.js locale configuration -// locale : afrikaans (af) -// author : Werner Mollentze : https://github.com/wernerm +//! moment.js locale configuration +//! locale : afrikaans (af) +//! author : Werner Mollentze : https://github.com/wernerm -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('af', { - months : "Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember".split("_"), - monthsShort : "Jan_Feb_Mar_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des".split("_"), - weekdays : "Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag".split("_"), - weekdaysShort : "Son_Maa_Din_Woe_Don_Vry_Sat".split("_"), - weekdaysMin : "So_Ma_Di_Wo_Do_Vr_Sa".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var af = moment.defineLocale('af', { + months : 'Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember'.split('_'), + monthsShort : 'Jan_Feb_Mar_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des'.split('_'), + weekdays : 'Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag'.split('_'), + weekdaysShort : 'Son_Maa_Din_Woe_Don_Vry_Sat'.split('_'), + weekdaysMin : 'So_Ma_Di_Wo_Do_Vr_Sa'.split('_'), + meridiemParse: /vm|nm/i, + isPM : function (input) { + return /^nm$/i.test(input); + }, meridiem : function (hours, minutes, isLower) { if (hours < 12) { return isLower ? 'vm' : 'VM'; @@ -25,11 +28,12 @@ } }, longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' }, calendar : { sameDay : '[Vandag om] LT', @@ -40,20 +44,21 @@ sameElse : 'L' }, relativeTime : { - future : "oor %s", - past : "%s gelede", - s : "'n paar sekondes", - m : "'n minuut", - mm : "%d minute", - h : "'n uur", - hh : "%d ure", - d : "'n dag", - dd : "%d dae", - M : "'n maand", - MM : "%d maande", - y : "'n jaar", - yy : "%d jaar" + future : 'oor %s', + past : '%s gelede', + s : '\'n paar sekondes', + m : '\'n minuut', + mm : '%d minute', + h : '\'n uur', + hh : '%d ure', + d : '\'n dag', + dd : '%d dae', + M : '\'n maand', + MM : '%d maande', + y : '\'n jaar', + yy : '%d jaar' }, + ordinalParse: /\d{1,2}(ste|de)/, ordinal : function (number) { return number + ((number === 1 || number === 8 || number >= 20) ? 'ste' : 'de'); // Thanks to Joris Röling : https://github.com/jjupiter }, @@ -62,4 +67,7 @@ doy : 4 // Die week wat die 4de Januarie bevat is die eerste week van die jaar. } }); -})); + + return af; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ar-ma.js b/lib/javascripts/moment_locale/ar-ma.js index c8add2ddeb..9bddd5ac4f 100644 --- a/lib/javascripts/moment_locale/ar-ma.js +++ b/lib/javascripts/moment_locale/ar-ma.js @@ -1,32 +1,32 @@ -// moment.js locale configuration -// locale : Moroccan Arabic (ar-ma) -// author : ElFadili Yassine : https://github.com/ElFadiliY -// author : Abdel Said : https://github.com/abdelsaid +//! moment.js locale configuration +//! locale : Moroccan Arabic (ar-ma) +//! author : ElFadili Yassine : https://github.com/ElFadiliY +//! author : Abdel Said : https://github.com/abdelsaid -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('ar-ma', { - months : "يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"), - monthsShort : "يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"), - weekdays : "الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"), - weekdaysShort : "احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت".split("_"), - weekdaysMin : "ح_ن_ث_ر_خ_ج_س".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var ar_ma = moment.defineLocale('ar-ma', { + months : 'يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر'.split('_'), + monthsShort : 'يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر'.split('_'), + weekdays : 'الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'), + weekdaysShort : 'احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت'.split('_'), + weekdaysMin : 'ح_ن_ث_ر_خ_ج_س'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' }, calendar : { - sameDay: "[اليوم على الساعة] LT", + sameDay: '[اليوم على الساعة] LT', nextDay: '[غدا على الساعة] LT', nextWeek: 'dddd [على الساعة] LT', lastDay: '[أمس على الساعة] LT', @@ -34,23 +34,26 @@ sameElse: 'L' }, relativeTime : { - future : "في %s", - past : "منذ %s", - s : "ثوان", - m : "دقيقة", - mm : "%d دقائق", - h : "ساعة", - hh : "%d ساعات", - d : "يوم", - dd : "%d أيام", - M : "شهر", - MM : "%d أشهر", - y : "سنة", - yy : "%d سنوات" + future : 'في %s', + past : 'منذ %s', + s : 'ثوان', + m : 'دقيقة', + mm : '%d دقائق', + h : 'ساعة', + hh : '%d ساعات', + d : 'يوم', + dd : '%d أيام', + M : 'شهر', + MM : '%d أشهر', + y : 'سنة', + yy : '%d سنوات' }, week : { dow : 6, // Saturday is the first day of the week. doy : 12 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return ar_ma; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ar-sa.js b/lib/javascripts/moment_locale/ar-sa.js index 64e209137e..7541c52dc1 100644 --- a/lib/javascripts/moment_locale/ar-sa.js +++ b/lib/javascripts/moment_locale/ar-sa.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : Arabic Saudi Arabia (ar-sa) -// author : Suhail Alkowaileet : https://github.com/xsoh +//! moment.js locale configuration +//! locale : Arabic Saudi Arabia (ar-sa) +//! author : Suhail Alkowaileet : https://github.com/xsoh + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var symbolMap = { '1': '١', '2': '٢', @@ -35,28 +34,33 @@ '٠': '0' }; - return moment.defineLocale('ar-sa', { - months : "يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"), - monthsShort : "يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"), - weekdays : "الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"), - weekdaysShort : "أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"), - weekdaysMin : "ح_ن_ث_ر_خ_ج_س".split("_"), + var ar_sa = moment.defineLocale('ar-sa', { + months : 'يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_'), + monthsShort : 'يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_'), + weekdays : 'الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'), + weekdaysShort : 'أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت'.split('_'), + weekdaysMin : 'ح_ن_ث_ر_خ_ج_س'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' + }, + meridiemParse: /ص|م/, + isPM : function (input) { + return 'م' === input; }, meridiem : function (hour, minute, isLower) { if (hour < 12) { - return "ص"; + return 'ص'; } else { - return "م"; + return 'م'; } }, calendar : { - sameDay: "[اليوم على الساعة] LT", + sameDay: '[اليوم على الساعة] LT', nextDay: '[غدا على الساعة] LT', nextWeek: 'dddd [على الساعة] LT', lastDay: '[أمس على الساعة] LT', @@ -64,22 +68,22 @@ sameElse: 'L' }, relativeTime : { - future : "في %s", - past : "منذ %s", - s : "ثوان", - m : "دقيقة", - mm : "%d دقائق", - h : "ساعة", - hh : "%d ساعات", - d : "يوم", - dd : "%d أيام", - M : "شهر", - MM : "%d أشهر", - y : "سنة", - yy : "%d سنوات" + future : 'في %s', + past : 'منذ %s', + s : 'ثوان', + m : 'دقيقة', + mm : '%d دقائق', + h : 'ساعة', + hh : '%d ساعات', + d : 'يوم', + dd : '%d أيام', + M : 'شهر', + MM : '%d أشهر', + y : 'سنة', + yy : '%d سنوات' }, preparse: function (string) { - return string.replace(/[۰-۹]/g, function (match) { + return string.replace(/[١٢٣٤٥٦٧٨٩٠]/g, function (match) { return numberMap[match]; }).replace(/،/g, ','); }, @@ -93,4 +97,7 @@ doy : 12 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return ar_sa; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ar-tn.js b/lib/javascripts/moment_locale/ar-tn.js new file mode 100644 index 0000000000..b4ee8fc6de --- /dev/null +++ b/lib/javascripts/moment_locale/ar-tn.js @@ -0,0 +1,57 @@ +//! moment.js locale configuration +//! locale : Tunisian Arabic (ar-tn) + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var ar_tn = moment.defineLocale('ar-tn', { + months: 'جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_'), + monthsShort: 'جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_'), + weekdays: 'الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'), + weekdaysShort: 'أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت'.split('_'), + weekdaysMin: 'ح_ن_ث_ر_خ_ج_س'.split('_'), + longDateFormat: { + LT: 'HH:mm', + LTS: 'HH:mm:ss', + L: 'DD/MM/YYYY', + LL: 'D MMMM YYYY', + LLL: 'D MMMM YYYY HH:mm', + LLLL: 'dddd D MMMM YYYY HH:mm' + }, + calendar: { + sameDay: '[اليوم على الساعة] LT', + nextDay: '[غدا على الساعة] LT', + nextWeek: 'dddd [على الساعة] LT', + lastDay: '[أمس على الساعة] LT', + lastWeek: 'dddd [على الساعة] LT', + sameElse: 'L' + }, + relativeTime: { + future: 'في %s', + past: 'منذ %s', + s: 'ثوان', + m: 'دقيقة', + mm: '%d دقائق', + h: 'ساعة', + hh: '%d ساعات', + d: 'يوم', + dd: '%d أيام', + M: 'شهر', + MM: '%d أشهر', + y: 'سنة', + yy: '%d سنوات' + }, + week: { + dow: 1, // Monday is the first day of the week. + doy: 4 // The week that contains Jan 4th is the first week of the year. + } + }); + + return ar_tn; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ar.js b/lib/javascripts/moment_locale/ar.js index 2af64ee3a9..3613c594b4 100644 --- a/lib/javascripts/moment_locale/ar.js +++ b/lib/javascripts/moment_locale/ar.js @@ -1,17 +1,17 @@ -// moment.js locale configuration -// locale : Arabic (ar) -// author : Abdel Said : https://github.com/abdelsaid -// changes in months, weekdays : Ahmed Elkhatib +//! moment.js locale configuration +//! Locale: Arabic (ar) +//! Author: Abdel Said: https://github.com/abdelsaid +//! Changes in months, weekdays: Ahmed Elkhatib +//! Native plural forms: forabi https://github.com/forabi + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var symbolMap = { '1': '١', '2': '٢', @@ -34,53 +34,89 @@ '٨': '8', '٩': '9', '٠': '0' - }; + }, pluralForm = function (n) { + return n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5; + }, plurals = { + s : ['أقل من ثانية', 'ثانية واحدة', ['ثانيتان', 'ثانيتين'], '%d ثوان', '%d ثانية', '%d ثانية'], + m : ['أقل من دقيقة', 'دقيقة واحدة', ['دقيقتان', 'دقيقتين'], '%d دقائق', '%d دقيقة', '%d دقيقة'], + h : ['أقل من ساعة', 'ساعة واحدة', ['ساعتان', 'ساعتين'], '%d ساعات', '%d ساعة', '%d ساعة'], + d : ['أقل من يوم', 'يوم واحد', ['يومان', 'يومين'], '%d أيام', '%d يومًا', '%d يوم'], + M : ['أقل من شهر', 'شهر واحد', ['شهران', 'شهرين'], '%d أشهر', '%d شهرا', '%d شهر'], + y : ['أقل من عام', 'عام واحد', ['عامان', 'عامين'], '%d أعوام', '%d عامًا', '%d عام'] + }, pluralize = function (u) { + return function (number, withoutSuffix, string, isFuture) { + var f = pluralForm(number), + str = plurals[u][pluralForm(number)]; + if (f === 2) { + str = str[withoutSuffix ? 0 : 1]; + } + return str.replace(/%d/i, number); + }; + }, months = [ + 'كانون الثاني يناير', + 'شباط فبراير', + 'آذار مارس', + 'نيسان أبريل', + 'أيار مايو', + 'حزيران يونيو', + 'تموز يوليو', + 'آب أغسطس', + 'أيلول سبتمبر', + 'تشرين الأول أكتوبر', + 'تشرين الثاني نوفمبر', + 'كانون الأول ديسمبر' + ]; - return moment.defineLocale('ar', { - months : "يناير/ كانون الثاني_فبراير/ شباط_مارس/ آذار_أبريل/ نيسان_مايو/ أيار_يونيو/ حزيران_يوليو/ تموز_أغسطس/ آب_سبتمبر/ أيلول_أكتوبر/ تشرين الأول_نوفمبر/ تشرين الثاني_ديسمبر/ كانون الأول".split("_"), - monthsShort : "يناير/ كانون الثاني_فبراير/ شباط_مارس/ آذار_أبريل/ نيسان_مايو/ أيار_يونيو/ حزيران_يوليو/ تموز_أغسطس/ آب_سبتمبر/ أيلول_أكتوبر/ تشرين الأول_نوفمبر/ تشرين الثاني_ديسمبر/ كانون الأول".split("_"), - weekdays : "الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"), - weekdaysShort : "أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"), - weekdaysMin : "ح_ن_ث_ر_خ_ج_س".split("_"), + var ar = moment.defineLocale('ar', { + months : months, + monthsShort : months, + weekdays : 'الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'), + weekdaysShort : 'أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت'.split('_'), + weekdaysMin : 'ح_ن_ث_ر_خ_ج_س'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'D/\u200FM/\u200FYYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' + }, + meridiemParse: /ص|م/, + isPM : function (input) { + return 'م' === input; }, meridiem : function (hour, minute, isLower) { if (hour < 12) { - return "ص"; + return 'ص'; } else { - return "م"; + return 'م'; } }, calendar : { - sameDay: "[اليوم على الساعة] LT", - nextDay: '[غدا على الساعة] LT', - nextWeek: 'dddd [على الساعة] LT', - lastDay: '[أمس على الساعة] LT', - lastWeek: 'dddd [على الساعة] LT', + sameDay: '[اليوم عند الساعة] LT', + nextDay: '[غدًا عند الساعة] LT', + nextWeek: 'dddd [عند الساعة] LT', + lastDay: '[أمس عند الساعة] LT', + lastWeek: 'dddd [عند الساعة] LT', sameElse: 'L' }, relativeTime : { - future : "في %s", - past : "منذ %s", - s : "ثوان", - m : "دقيقة", - mm : "%d دقائق", - h : "ساعة", - hh : "%d ساعات", - d : "يوم", - dd : "%d أيام", - M : "شهر", - MM : "%d أشهر", - y : "سنة", - yy : "%d سنوات" + future : 'بعد %s', + past : 'منذ %s', + s : pluralize('s'), + m : pluralize('m'), + mm : pluralize('m'), + h : pluralize('h'), + hh : pluralize('h'), + d : pluralize('d'), + dd : pluralize('d'), + M : pluralize('M'), + MM : pluralize('M'), + y : pluralize('y'), + yy : pluralize('y') }, preparse: function (string) { - return string.replace(/[۰-۹]/g, function (match) { + return string.replace(/\u200f/g, '').replace(/[١٢٣٤٥٦٧٨٩٠]/g, function (match) { return numberMap[match]; }).replace(/،/g, ','); }, @@ -94,4 +130,7 @@ doy : 12 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return ar; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/az.js b/lib/javascripts/moment_locale/az.js index a6a5aff964..5ff9b088c4 100644 --- a/lib/javascripts/moment_locale/az.js +++ b/lib/javascripts/moment_locale/az.js @@ -1,53 +1,49 @@ -// moment.js locale configuration -// locale : azerbaijani (az) -// author : topchiyev : https://github.com/topchiyev +//! moment.js locale configuration +//! locale : azerbaijani (az) +//! author : topchiyev : https://github.com/topchiyev + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var suffixes = { - 1: "-inci", - 5: "-inci", - 8: "-inci", - 70: "-inci", - 80: "-inci", - - 2: "-nci", - 7: "-nci", - 20: "-nci", - 50: "-nci", - - 3: "-üncü", - 4: "-üncü", - 100: "-üncü", - - 6: "-ncı", - - 9: "-uncu", - 10: "-uncu", - 30: "-uncu", - - 60: "-ıncı", - 90: "-ıncı" + 1: '-inci', + 5: '-inci', + 8: '-inci', + 70: '-inci', + 80: '-inci', + 2: '-nci', + 7: '-nci', + 20: '-nci', + 50: '-nci', + 3: '-üncü', + 4: '-üncü', + 100: '-üncü', + 6: '-ncı', + 9: '-uncu', + 10: '-uncu', + 30: '-uncu', + 60: '-ıncı', + 90: '-ıncı' }; - return moment.defineLocale('az', { - months : "yanvar_fevral_mart_aprel_may_iyun_iyul_avqust_sentyabr_oktyabr_noyabr_dekabr".split("_"), - monthsShort : "yan_fev_mar_apr_may_iyn_iyl_avq_sen_okt_noy_dek".split("_"), - weekdays : "Bazar_Bazar ertəsi_Çərşənbə axşamı_Çərşənbə_Cümə axşamı_Cümə_Şənbə".split("_"), - weekdaysShort : "Baz_BzE_ÇAx_Çər_CAx_Cüm_Şən".split("_"), - weekdaysMin : "Bz_BE_ÇA_Çə_CA_Cü_Şə".split("_"), + + var az = moment.defineLocale('az', { + months : 'yanvar_fevral_mart_aprel_may_iyun_iyul_avqust_sentyabr_oktyabr_noyabr_dekabr'.split('_'), + monthsShort : 'yan_fev_mar_apr_may_iyn_iyl_avq_sen_okt_noy_dek'.split('_'), + weekdays : 'Bazar_Bazar ertəsi_Çərşənbə axşamı_Çərşənbə_Cümə axşamı_Cümə_Şənbə'.split('_'), + weekdaysShort : 'Baz_BzE_ÇAx_Çər_CAx_Cüm_Şən'.split('_'), + weekdaysMin : 'Bz_BE_ÇA_Çə_CA_Cü_Şə'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD.MM.YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' }, calendar : { sameDay : '[bugün saat] LT', @@ -58,39 +54,43 @@ sameElse : 'L' }, relativeTime : { - future : "%s sonra", - past : "%s əvvəl", - s : "birneçə saniyyə", - m : "bir dəqiqə", - mm : "%d dəqiqə", - h : "bir saat", - hh : "%d saat", - d : "bir gün", - dd : "%d gün", - M : "bir ay", - MM : "%d ay", - y : "bir il", - yy : "%d il" + future : '%s sonra', + past : '%s əvvəl', + s : 'birneçə saniyyə', + m : 'bir dəqiqə', + mm : '%d dəqiqə', + h : 'bir saat', + hh : '%d saat', + d : 'bir gün', + dd : '%d gün', + M : 'bir ay', + MM : '%d ay', + y : 'bir il', + yy : '%d il' + }, + meridiemParse: /gecə|səhər|gündüz|axşam/, + isPM : function (input) { + return /^(gündüz|axşam)$/.test(input); }, meridiem : function (hour, minute, isLower) { if (hour < 4) { - return "gecə"; + return 'gecə'; } else if (hour < 12) { - return "səhər"; + return 'səhər'; } else if (hour < 17) { - return "gündüz"; + return 'gündüz'; } else { - return "axşam"; + return 'axşam'; } }, + ordinalParse: /\d{1,2}-(ıncı|inci|nci|üncü|ncı|uncu)/, ordinal : function (number) { if (number === 0) { // special case for zero - return number + "-ıncı"; + return number + '-ıncı'; } var a = number % 10, b = number % 100 - a, c = number >= 100 ? 100 : null; - return number + (suffixes[a] || suffixes[b] || suffixes[c]); }, week : { @@ -98,4 +98,7 @@ doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return az; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/be.js b/lib/javascripts/moment_locale/be.js index 6e0aef1b8d..c6294b34f4 100644 --- a/lib/javascripts/moment_locale/be.js +++ b/lib/javascripts/moment_locale/be.js @@ -1,23 +1,21 @@ -// moment.js locale configuration -// locale : belarusian (be) -// author : Dmitry Demidov : https://github.com/demidov91 -// author: Praleska: http://praleska.pro/ -// Author : Menelion Elensúle : https://github.com/Oire +//! moment.js locale configuration +//! locale : belarusian (be) +//! author : Dmitry Demidov : https://github.com/demidov91 +//! author: Praleska: http://praleska.pro/ +//! Author : Menelion Elensúle : https://github.com/Oire + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function plural(word, num) { var forms = word.split('_'); return num % 10 === 1 && num % 100 !== 11 ? forms[0] : (num % 10 >= 2 && num % 10 <= 4 && (num % 100 < 10 || num % 100 >= 20) ? forms[1] : forms[2]); } - function relativeTimeWithPlural(number, withoutSuffix, key) { var format = { 'mm': withoutSuffix ? 'хвіліна_хвіліны_хвілін' : 'хвіліну_хвіліны_хвілін', @@ -37,44 +35,26 @@ } } - function monthsCaseReplace(m, format) { - var months = { - 'nominative': 'студзень_люты_сакавік_красавік_травень_чэрвень_ліпень_жнівень_верасень_кастрычнік_лістапад_снежань'.split('_'), - 'accusative': 'студзеня_лютага_сакавіка_красавіка_траўня_чэрвеня_ліпеня_жніўня_верасня_кастрычніка_лістапада_снежня'.split('_') + var be = moment.defineLocale('be', { + months : { + format: 'студзеня_лютага_сакавіка_красавіка_траўня_чэрвеня_ліпеня_жніўня_верасня_кастрычніка_лістапада_снежня'.split('_'), + standalone: 'студзень_люты_сакавік_красавік_травень_чэрвень_ліпень_жнівень_верасень_кастрычнік_лістапад_снежань'.split('_') }, - - nounCase = (/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/).test(format) ? - 'accusative' : - 'nominative'; - - return months[nounCase][m.month()]; - } - - function weekdaysCaseReplace(m, format) { - var weekdays = { - 'nominative': 'нядзеля_панядзелак_аўторак_серада_чацвер_пятніца_субота'.split('_'), - 'accusative': 'нядзелю_панядзелак_аўторак_сераду_чацвер_пятніцу_суботу'.split('_') - }, - - nounCase = (/\[ ?[Вв] ?(?:мінулую|наступную)? ?\] ?dddd/).test(format) ? - 'accusative' : - 'nominative'; - - return weekdays[nounCase][m.day()]; - } - - return moment.defineLocale('be', { - months : monthsCaseReplace, monthsShort : 'студ_лют_сак_крас_трав_чэрв_ліп_жнів_вер_каст_ліст_снеж'.split('_'), - weekdays : weekdaysCaseReplace, - weekdaysShort : "нд_пн_ат_ср_чц_пт_сб".split("_"), - weekdaysMin : "нд_пн_ат_ср_чц_пт_сб".split("_"), + weekdays : { + format: 'нядзелю_панядзелак_аўторак_сераду_чацвер_пятніцу_суботу'.split('_'), + standalone: 'нядзеля_панядзелак_аўторак_серада_чацвер_пятніца_субота'.split('_'), + isFormat: /\[ ?[Вв] ?(?:мінулую|наступную)? ?\] ?dddd/ + }, + weekdaysShort : 'нд_пн_ат_ср_чц_пт_сб'.split('_'), + weekdaysMin : 'нд_пн_ат_ср_чц_пт_сб'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD.MM.YYYY", - LL : "D MMMM YYYY г.", - LLL : "D MMMM YYYY г., LT", - LLLL : "dddd, D MMMM YYYY г., LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY г.', + LLL : 'D MMMM YYYY г., HH:mm', + LLLL : 'dddd, D MMMM YYYY г., HH:mm' }, calendar : { sameDay: '[Сёння ў] LT', @@ -99,34 +79,36 @@ sameElse: 'L' }, relativeTime : { - future : "праз %s", - past : "%s таму", - s : "некалькі секунд", + future : 'праз %s', + past : '%s таму', + s : 'некалькі секунд', m : relativeTimeWithPlural, mm : relativeTimeWithPlural, h : relativeTimeWithPlural, hh : relativeTimeWithPlural, - d : "дзень", + d : 'дзень', dd : relativeTimeWithPlural, - M : "месяц", + M : 'месяц', MM : relativeTimeWithPlural, - y : "год", + y : 'год', yy : relativeTimeWithPlural }, - - + meridiemParse: /ночы|раніцы|дня|вечара/, + isPM : function (input) { + return /^(дня|вечара)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { - return "ночы"; + return 'ночы'; } else if (hour < 12) { - return "раніцы"; + return 'раніцы'; } else if (hour < 17) { - return "дня"; + return 'дня'; } else { - return "вечара"; + return 'вечара'; } }, - + ordinalParse: /\d{1,2}-(і|ы|га)/, ordinal: function (number, period) { switch (period) { case 'M': @@ -141,10 +123,12 @@ return number; } }, - week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return be; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/bg.js b/lib/javascripts/moment_locale/bg.js index b8a8c326ad..169e1238a1 100644 --- a/lib/javascripts/moment_locale/bg.js +++ b/lib/javascripts/moment_locale/bg.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : bulgarian (bg) -// author : Krasen Borisov : https://github.com/kraz +//! moment.js locale configuration +//! locale : bulgarian (bg) +//! author : Krasen Borisov : https://github.com/kraz -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('bg', { - months : "януари_февруари_март_април_май_юни_юли_август_септември_октомври_ноември_декември".split("_"), - monthsShort : "янр_фев_мар_апр_май_юни_юли_авг_сеп_окт_ное_дек".split("_"), - weekdays : "неделя_понеделник_вторник_сряда_четвъртък_петък_събота".split("_"), - weekdaysShort : "нед_пон_вто_сря_чет_пет_съб".split("_"), - weekdaysMin : "нд_пн_вт_ср_чт_пт_сб".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var bg = moment.defineLocale('bg', { + months : 'януари_февруари_март_април_май_юни_юли_август_септември_октомври_ноември_декември'.split('_'), + monthsShort : 'янр_фев_мар_апр_май_юни_юли_авг_сеп_окт_ное_дек'.split('_'), + weekdays : 'неделя_понеделник_вторник_сряда_четвъртък_петък_събота'.split('_'), + weekdaysShort : 'нед_пон_вто_сря_чет_пет_съб'.split('_'), + weekdaysMin : 'нд_пн_вт_ср_чт_пт_сб'.split('_'), longDateFormat : { - LT : "H:mm", - L : "D.MM.YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'D.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY H:mm', + LLLL : 'dddd, D MMMM YYYY H:mm' }, calendar : { sameDay : '[Днес в] LT', @@ -45,20 +45,21 @@ sameElse : 'L' }, relativeTime : { - future : "след %s", - past : "преди %s", - s : "няколко секунди", - m : "минута", - mm : "%d минути", - h : "час", - hh : "%d часа", - d : "ден", - dd : "%d дни", - M : "месец", - MM : "%d месеца", - y : "година", - yy : "%d години" + future : 'след %s', + past : 'преди %s', + s : 'няколко секунди', + m : 'минута', + mm : '%d минути', + h : 'час', + hh : '%d часа', + d : 'ден', + dd : '%d дни', + M : 'месец', + MM : '%d месеца', + y : 'година', + yy : '%d години' }, + ordinalParse: /\d{1,2}-(ев|ен|ти|ви|ри|ми)/, ordinal : function (number) { var lastDigit = number % 10, last2Digits = number % 100; @@ -83,4 +84,7 @@ doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return bg; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/bn.js b/lib/javascripts/moment_locale/bn.js index 8ceb8eb0a4..4eca5ee9eb 100644 --- a/lib/javascripts/moment_locale/bn.js +++ b/lib/javascripts/moment_locale/bn.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : Bengali (bn) -// author : Kaushik Gandhi : https://github.com/kaushikgandhi +//! moment.js locale configuration +//! locale : Bengali (bn) +//! author : Kaushik Gandhi : https://github.com/kaushikgandhi + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var symbolMap = { '1': '১', '2': '২', @@ -36,18 +35,19 @@ '০': '0' }; - return moment.defineLocale('bn', { - months : 'জানুয়ারী_ফেবুয়ারী_মার্চ_এপ্রিল_মে_জুন_জুলাই_অগাস্ট_সেপ্টেম্বর_অক্টোবর_নভেম্বর_ডিসেম্বর'.split("_"), - monthsShort : 'জানু_ফেব_মার্চ_এপর_মে_জুন_জুল_অগ_সেপ্ট_অক্টো_নভ_ডিসেম্'.split("_"), - weekdays : 'রবিবার_সোমবার_মঙ্গলবার_বুধবার_বৃহস্পত্তিবার_শুক্রুবার_শনিবার'.split("_"), - weekdaysShort : 'রবি_সোম_মঙ্গল_বুধ_বৃহস্পত্তি_শুক্রু_শনি'.split("_"), - weekdaysMin : 'রব_সম_মঙ্গ_বু_ব্রিহ_শু_শনি'.split("_"), + var bn = moment.defineLocale('bn', { + months : 'জানুয়ারী_ফেবুয়ারী_মার্চ_এপ্রিল_মে_জুন_জুলাই_অগাস্ট_সেপ্টেম্বর_অক্টোবর_নভেম্বর_ডিসেম্বর'.split('_'), + monthsShort : 'জানু_ফেব_মার্চ_এপর_মে_জুন_জুল_অগ_সেপ্ট_অক্টো_নভ_ডিসেম্'.split('_'), + weekdays : 'রবিবার_সোমবার_মঙ্গলবার_বুধবার_বৃহস্পত্তিবার_শুক্রবার_শনিবার'.split('_'), + weekdaysShort : 'রবি_সোম_মঙ্গল_বুধ_বৃহস্পত্তি_শুক্র_শনি'.split('_'), + weekdaysMin : 'রব_সম_মঙ্গ_বু_ব্রিহ_শু_শনি'.split('_'), longDateFormat : { - LT : "A h:mm সময়", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY, LT", - LLLL : "dddd, D MMMM YYYY, LT" + LT : 'A h:mm সময়', + LTS : 'A h:mm:ss সময়', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY, A h:mm সময়', + LLLL : 'dddd, D MMMM YYYY, A h:mm সময়' }, calendar : { sameDay : '[আজ] LT', @@ -58,19 +58,19 @@ sameElse : 'L' }, relativeTime : { - future : "%s পরে", - past : "%s আগে", - s : "কএক সেকেন্ড", - m : "এক মিনিট", - mm : "%d মিনিট", - h : "এক ঘন্টা", - hh : "%d ঘন্টা", - d : "এক দিন", - dd : "%d দিন", - M : "এক মাস", - MM : "%d মাস", - y : "এক বছর", - yy : "%d বছর" + future : '%s পরে', + past : '%s আগে', + s : 'কয়েক সেকেন্ড', + m : 'এক মিনিট', + mm : '%d মিনিট', + h : 'এক ঘন্টা', + hh : '%d ঘন্টা', + d : 'এক দিন', + dd : '%d দিন', + M : 'এক মাস', + MM : '%d মাস', + y : 'এক বছর', + yy : '%d বছর' }, preparse: function (string) { return string.replace(/[১২৩৪৫৬৭৮৯০]/g, function (match) { @@ -82,20 +82,24 @@ return symbolMap[match]; }); }, + meridiemParse: /রাত|সকাল|দুপুর|বিকাল|রাত/, + isPM: function (input) { + return /^(দুপুর|বিকাল|রাত)$/.test(input); + }, //Bengali is a vast language its spoken //in different forms in various parts of the world. //I have just generalized with most common one used meridiem : function (hour, minute, isLower) { if (hour < 4) { - return "রাত"; + return 'রাত'; } else if (hour < 10) { - return "শকাল"; + return 'সকাল'; } else if (hour < 17) { - return "দুপুর"; + return 'দুপুর'; } else if (hour < 20) { - return "বিকেল"; + return 'বিকাল'; } else { - return "রাত"; + return 'রাত'; } }, week : { @@ -103,4 +107,7 @@ doy : 6 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return bn; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/bo.js b/lib/javascripts/moment_locale/bo.js index f1567abf5e..3ab33896f0 100644 --- a/lib/javascripts/moment_locale/bo.js +++ b/lib/javascripts/moment_locale/bo.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : tibetan (bo) -// author : Thupten N. Chakrishar : https://github.com/vajradog +//! moment.js locale configuration +//! locale : tibetan (bo) +//! author : Thupten N. Chakrishar : https://github.com/vajradog + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var symbolMap = { '1': '༡', '2': '༢', @@ -36,18 +35,19 @@ '༠': '0' }; - return moment.defineLocale('bo', { - months : 'ཟླ་བ་དང་པོ_ཟླ་བ་གཉིས་པ_ཟླ་བ་གསུམ་པ_ཟླ་བ་བཞི་པ_ཟླ་བ་ལྔ་པ_ཟླ་བ་དྲུག་པ_ཟླ་བ་བདུན་པ_ཟླ་བ་བརྒྱད་པ_ཟླ་བ་དགུ་པ_ཟླ་བ་བཅུ་པ_ཟླ་བ་བཅུ་གཅིག་པ_ཟླ་བ་བཅུ་གཉིས་པ'.split("_"), - monthsShort : 'ཟླ་བ་དང་པོ_ཟླ་བ་གཉིས་པ_ཟླ་བ་གསུམ་པ_ཟླ་བ་བཞི་པ_ཟླ་བ་ལྔ་པ_ཟླ་བ་དྲུག་པ_ཟླ་བ་བདུན་པ_ཟླ་བ་བརྒྱད་པ_ཟླ་བ་དགུ་པ_ཟླ་བ་བཅུ་པ_ཟླ་བ་བཅུ་གཅིག་པ_ཟླ་བ་བཅུ་གཉིས་པ'.split("_"), - weekdays : 'གཟའ་ཉི་མ་_གཟའ་ཟླ་བ་_གཟའ་མིག་དམར་_གཟའ་ལྷག་པ་_གཟའ་ཕུར་བུ_གཟའ་པ་སངས་_གཟའ་སྤེན་པ་'.split("_"), - weekdaysShort : 'ཉི་མ་_ཟླ་བ་_མིག་དམར་_ལྷག་པ་_ཕུར་བུ_པ་སངས་_སྤེན་པ་'.split("_"), - weekdaysMin : 'ཉི་མ་_ཟླ་བ་_མིག་དམར་_ལྷག་པ་_ཕུར་བུ_པ་སངས་_སྤེན་པ་'.split("_"), + var bo = moment.defineLocale('bo', { + months : 'ཟླ་བ་དང་པོ_ཟླ་བ་གཉིས་པ_ཟླ་བ་གསུམ་པ_ཟླ་བ་བཞི་པ_ཟླ་བ་ལྔ་པ_ཟླ་བ་དྲུག་པ_ཟླ་བ་བདུན་པ_ཟླ་བ་བརྒྱད་པ_ཟླ་བ་དགུ་པ_ཟླ་བ་བཅུ་པ_ཟླ་བ་བཅུ་གཅིག་པ_ཟླ་བ་བཅུ་གཉིས་པ'.split('_'), + monthsShort : 'ཟླ་བ་དང་པོ_ཟླ་བ་གཉིས་པ_ཟླ་བ་གསུམ་པ_ཟླ་བ་བཞི་པ_ཟླ་བ་ལྔ་པ_ཟླ་བ་དྲུག་པ_ཟླ་བ་བདུན་པ_ཟླ་བ་བརྒྱད་པ_ཟླ་བ་དགུ་པ_ཟླ་བ་བཅུ་པ_ཟླ་བ་བཅུ་གཅིག་པ_ཟླ་བ་བཅུ་གཉིས་པ'.split('_'), + weekdays : 'གཟའ་ཉི་མ་_གཟའ་ཟླ་བ་_གཟའ་མིག་དམར་_གཟའ་ལྷག་པ་_གཟའ་ཕུར་བུ_གཟའ་པ་སངས་_གཟའ་སྤེན་པ་'.split('_'), + weekdaysShort : 'ཉི་མ་_ཟླ་བ་_མིག་དམར་_ལྷག་པ་_ཕུར་བུ_པ་སངས་_སྤེན་པ་'.split('_'), + weekdaysMin : 'ཉི་མ་_ཟླ་བ་_མིག་དམར་_ལྷག་པ་_ཕུར་བུ_པ་སངས་_སྤེན་པ་'.split('_'), longDateFormat : { - LT : "A h:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY, LT", - LLLL : "dddd, D MMMM YYYY, LT" + LT : 'A h:mm', + LTS : 'A h:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY, A h:mm', + LLLL : 'dddd, D MMMM YYYY, A h:mm' }, calendar : { sameDay : '[དི་རིང] LT', @@ -58,19 +58,19 @@ sameElse : 'L' }, relativeTime : { - future : "%s ལ་", - past : "%s སྔན་ལ", - s : "ལམ་སང", - m : "སྐར་མ་གཅིག", - mm : "%d སྐར་མ", - h : "ཆུ་ཚོད་གཅིག", - hh : "%d ཆུ་ཚོད", - d : "ཉིན་གཅིག", - dd : "%d ཉིན་", - M : "ཟླ་བ་གཅིག", - MM : "%d ཟླ་བ", - y : "ལོ་གཅིག", - yy : "%d ལོ" + future : '%s ལ་', + past : '%s སྔན་ལ', + s : 'ལམ་སང', + m : 'སྐར་མ་གཅིག', + mm : '%d སྐར་མ', + h : 'ཆུ་ཚོད་གཅིག', + hh : '%d ཆུ་ཚོད', + d : 'ཉིན་གཅིག', + dd : '%d ཉིན་', + M : 'ཟླ་བ་གཅིག', + MM : '%d ཟླ་བ', + y : 'ལོ་གཅིག', + yy : '%d ལོ' }, preparse: function (string) { return string.replace(/[༡༢༣༤༥༦༧༨༩༠]/g, function (match) { @@ -82,17 +82,21 @@ return symbolMap[match]; }); }, + meridiemParse: /མཚན་མོ|ཞོགས་ཀས|ཉིན་གུང|དགོང་དག|མཚན་མོ/, + isPM: function (input) { + return /^(ཉིན་གུང|དགོང་དག|མཚན་མོ)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { - return "མཚན་མོ"; + return 'མཚན་མོ'; } else if (hour < 10) { - return "ཞོགས་ཀས"; + return 'ཞོགས་ཀས'; } else if (hour < 17) { - return "ཉིན་གུང"; + return 'ཉིན་གུང'; } else if (hour < 20) { - return "དགོང་དག"; + return 'དགོང་དག'; } else { - return "མཚན་མོ"; + return 'མཚན་མོ'; } }, week : { @@ -100,4 +104,7 @@ doy : 6 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return bo; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/br.js b/lib/javascripts/moment_locale/br.js index fb11fe1e57..2896cfb6a4 100644 --- a/lib/javascripts/moment_locale/br.js +++ b/lib/javascripts/moment_locale/br.js @@ -1,25 +1,23 @@ -// moment.js locale configuration -// locale : breton (br) -// author : Jean-Baptiste Le Duigou : https://github.com/jbleduigou +//! moment.js locale configuration +//! locale : breton (br) +//! author : Jean-Baptiste Le Duigou : https://github.com/jbleduigou + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function relativeTimeWithMutation(number, withoutSuffix, key) { var format = { - 'mm': "munutenn", - 'MM': "miz", - 'dd': "devezh" + 'mm': 'munutenn', + 'MM': 'miz', + 'dd': 'devezh' }; return number + ' ' + mutation(format[key], number); } - function specialMutationForYears(number) { switch (lastNumber(number)) { case 1: @@ -32,21 +30,18 @@ return number + ' vloaz'; } } - function lastNumber(number) { if (number > 9) { return lastNumber(number % 10); } return number; } - function mutation(text, number) { if (number === 2) { return softMutation(text); } return text; } - function softMutation(text) { var mutationTable = { 'm': 'v', @@ -59,18 +54,19 @@ return mutationTable[text.charAt(0)] + text.substring(1); } - return moment.defineLocale('br', { - months : "Genver_C'hwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu".split("_"), - monthsShort : "Gen_C'hwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker".split("_"), - weekdays : "Sul_Lun_Meurzh_Merc'her_Yaou_Gwener_Sadorn".split("_"), - weekdaysShort : "Sul_Lun_Meu_Mer_Yao_Gwe_Sad".split("_"), - weekdaysMin : "Su_Lu_Me_Mer_Ya_Gw_Sa".split("_"), + var br = moment.defineLocale('br', { + months : 'Genver_C\'hwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu'.split('_'), + monthsShort : 'Gen_C\'hwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker'.split('_'), + weekdays : 'Sul_Lun_Meurzh_Merc\'her_Yaou_Gwener_Sadorn'.split('_'), + weekdaysShort : 'Sul_Lun_Meu_Mer_Yao_Gwe_Sad'.split('_'), + weekdaysMin : 'Su_Lu_Me_Mer_Ya_Gw_Sa'.split('_'), longDateFormat : { - LT : "h[e]mm A", - L : "DD/MM/YYYY", - LL : "D [a viz] MMMM YYYY", - LLL : "D [a viz] MMMM YYYY LT", - LLLL : "dddd, D [a viz] MMMM YYYY LT" + LT : 'h[e]mm A', + LTS : 'h[e]mm:ss A', + L : 'DD/MM/YYYY', + LL : 'D [a viz] MMMM YYYY', + LLL : 'D [a viz] MMMM YYYY h[e]mm A', + LLLL : 'dddd, D [a viz] MMMM YYYY h[e]mm A' }, calendar : { sameDay : '[Hiziv da] LT', @@ -81,20 +77,21 @@ sameElse : 'L' }, relativeTime : { - future : "a-benn %s", - past : "%s 'zo", - s : "un nebeud segondennoù", - m : "ur vunutenn", + future : 'a-benn %s', + past : '%s \'zo', + s : 'un nebeud segondennoù', + m : 'ur vunutenn', mm : relativeTimeWithMutation, - h : "un eur", - hh : "%d eur", - d : "un devezh", + h : 'un eur', + hh : '%d eur', + d : 'un devezh', dd : relativeTimeWithMutation, - M : "ur miz", + M : 'ur miz', MM : relativeTimeWithMutation, - y : "ur bloaz", + y : 'ur bloaz', yy : specialMutationForYears }, + ordinalParse: /\d{1,2}(añ|vet)/, ordinal : function (number) { var output = (number === 1) ? 'añ' : 'vet'; return number + output; @@ -104,4 +101,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return br; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/bs.js b/lib/javascripts/moment_locale/bs.js index d69015abbe..e0b3dae21e 100644 --- a/lib/javascripts/moment_locale/bs.js +++ b/lib/javascripts/moment_locale/bs.js @@ -1,19 +1,18 @@ -// moment.js locale configuration -// locale : bosnian (bs) -// author : Nedim Cholich : https://github.com/frontyard -// based on (hr) translation by Bojan Marković +//! moment.js locale configuration +//! locale : bosnian (bs) +//! author : Nedim Cholich : https://github.com/frontyard +//! based on (hr) translation by Bojan Marković + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function translate(number, withoutSuffix, key) { - var result = number + " "; + var result = number + ' '; switch (key) { case 'm': return withoutSuffix ? 'jedna minuta' : 'jedne minute'; @@ -65,23 +64,23 @@ } } - return moment.defineLocale('bs', { - months : "januar_februar_mart_april_maj_juni_juli_avgust_septembar_oktobar_novembar_decembar".split("_"), - monthsShort : "jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"), - weekdays : "nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota".split("_"), - weekdaysShort : "ned._pon._uto._sri._čet._pet._sub.".split("_"), - weekdaysMin : "ne_po_ut_sr_če_pe_su".split("_"), + var bs = moment.defineLocale('bs', { + months : 'januar_februar_mart_april_maj_juni_juli_august_septembar_oktobar_novembar_decembar'.split('_'), + monthsShort : 'jan._feb._mar._apr._maj._jun._jul._aug._sep._okt._nov._dec.'.split('_'), + weekdays : 'nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota'.split('_'), + weekdaysShort : 'ned._pon._uto._sri._čet._pet._sub.'.split('_'), + weekdaysMin : 'ne_po_ut_sr_če_pe_su'.split('_'), longDateFormat : { - LT : "H:mm", - L : "DD. MM. YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY LT", - LLLL : "dddd, D. MMMM YYYY LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'DD. MM. YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY H:mm', + LLLL : 'dddd, D. MMMM YYYY H:mm' }, calendar : { sameDay : '[danas u] LT', nextDay : '[sutra u] LT', - nextWeek : function () { switch (this.day()) { case 0: @@ -115,24 +114,28 @@ sameElse : 'L' }, relativeTime : { - future : "za %s", - past : "prije %s", - s : "par sekundi", + future : 'za %s', + past : 'prije %s', + s : 'par sekundi', m : translate, mm : translate, h : translate, hh : translate, - d : "dan", + d : 'dan', dd : translate, - M : "mjesec", + M : 'mjesec', MM : translate, - y : "godinu", + y : 'godinu', yy : translate }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return bs; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ca.js b/lib/javascripts/moment_locale/ca.js index 932c1cbda7..15f75fe99c 100644 --- a/lib/javascripts/moment_locale/ca.js +++ b/lib/javascripts/moment_locale/ca.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : catalan (ca) -// author : Juan G. Hurtado : https://github.com/juanghurtado +//! moment.js locale configuration +//! locale : catalan (ca) +//! author : Juan G. Hurtado : https://github.com/juanghurtado -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('ca', { - months : "gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"), - monthsShort : "gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.".split("_"), - weekdays : "diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"), - weekdaysShort : "dg._dl._dt._dc._dj._dv._ds.".split("_"), - weekdaysMin : "Dg_Dl_Dt_Dc_Dj_Dv_Ds".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var ca = moment.defineLocale('ca', { + months : 'gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre'.split('_'), + monthsShort : 'gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.'.split('_'), + weekdays : 'diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte'.split('_'), + weekdaysShort : 'dg._dl._dt._dc._dj._dv._ds.'.split('_'), + weekdaysMin : 'Dg_Dl_Dt_Dc_Dj_Dv_Ds'.split('_'), longDateFormat : { - LT : "H:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY H:mm', + LLLL : 'dddd D MMMM YYYY H:mm' }, calendar : { sameDay : function () { @@ -43,24 +43,37 @@ sameElse : 'L' }, relativeTime : { - future : "en %s", - past : "fa %s", - s : "uns segons", - m : "un minut", - mm : "%d minuts", - h : "una hora", - hh : "%d hores", - d : "un dia", - dd : "%d dies", - M : "un mes", - MM : "%d mesos", - y : "un any", - yy : "%d anys" + future : 'en %s', + past : 'fa %s', + s : 'uns segons', + m : 'un minut', + mm : '%d minuts', + h : 'una hora', + hh : '%d hores', + d : 'un dia', + dd : '%d dies', + M : 'un mes', + MM : '%d mesos', + y : 'un any', + yy : '%d anys' + }, + ordinalParse: /\d{1,2}(r|n|t|è|a)/, + ordinal : function (number, period) { + var output = (number === 1) ? 'r' : + (number === 2) ? 'n' : + (number === 3) ? 'r' : + (number === 4) ? 't' : 'è'; + if (period === 'w' || period === 'W') { + output = 'a'; + } + return number + output; }, - ordinal : '%dº', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return ca; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/cs.js b/lib/javascripts/moment_locale/cs.js index 085bba06f4..5854f70e2e 100644 --- a/lib/javascripts/moment_locale/cs.js +++ b/lib/javascripts/moment_locale/cs.js @@ -1,25 +1,22 @@ -// moment.js locale configuration -// locale : czech (cs) -// author : petrbela : https://github.com/petrbela +//! moment.js locale configuration +//! locale : czech (cs) +//! author : petrbela : https://github.com/petrbela -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - var months = "leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"), - monthsShort = "led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_"); +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + var months = 'leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec'.split('_'), + monthsShort = 'led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro'.split('_'); function plural(n) { return (n > 1) && (n < 5) && (~~(n / 10) !== 1); } - function translate(number, withoutSuffix, key, isFuture) { - var result = number + " "; + var result = number + ' '; switch (key) { case 's': // a few seconds / in a few seconds / a few seconds ago return (withoutSuffix || isFuture) ? 'pár sekund' : 'pár sekundami'; @@ -71,7 +68,7 @@ } } - return moment.defineLocale('cs', { + var cs = moment.defineLocale('cs', { months : months, monthsShort : monthsShort, monthsParse : (function (months, monthsShort) { @@ -82,18 +79,33 @@ } return _monthsParse; }(months, monthsShort)), - weekdays : "neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"), - weekdaysShort : "ne_po_út_st_čt_pá_so".split("_"), - weekdaysMin : "ne_po_út_st_čt_pá_so".split("_"), + shortMonthsParse : (function (monthsShort) { + var i, _shortMonthsParse = []; + for (i = 0; i < 12; i++) { + _shortMonthsParse[i] = new RegExp('^' + monthsShort[i] + '$', 'i'); + } + return _shortMonthsParse; + }(monthsShort)), + longMonthsParse : (function (months) { + var i, _longMonthsParse = []; + for (i = 0; i < 12; i++) { + _longMonthsParse[i] = new RegExp('^' + months[i] + '$', 'i'); + } + return _longMonthsParse; + }(months)), + weekdays : 'neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota'.split('_'), + weekdaysShort : 'ne_po_út_st_čt_pá_so'.split('_'), + weekdaysMin : 'ne_po_út_st_čt_pá_so'.split('_'), longDateFormat : { - LT: "H.mm", - L : "DD. MM. YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY LT", - LLLL : "dddd D. MMMM YYYY LT" + LT: 'H:mm', + LTS : 'H:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY H:mm', + LLLL : 'dddd D. MMMM YYYY H:mm' }, calendar : { - sameDay: "[dnes v] LT", + sameDay: '[dnes v] LT', nextDay: '[zítra v] LT', nextWeek: function () { switch (this.day()) { @@ -129,11 +141,11 @@ return '[minulou sobotu v] LT'; } }, - sameElse: "L" + sameElse: 'L' }, relativeTime : { - future : "za %s", - past : "před %s", + future : 'za %s', + past : 'před %s', s : translate, m : translate, mm : translate, @@ -146,10 +158,14 @@ y : translate, yy : translate }, + ordinalParse : /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return cs; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/cv.js b/lib/javascripts/moment_locale/cv.js index 0a290d8faf..a1d87e1846 100644 --- a/lib/javascripts/moment_locale/cv.js +++ b/lib/javascripts/moment_locale/cv.js @@ -1,59 +1,63 @@ -// moment.js locale configuration -// locale : chuvash (cv) -// author : Anatoly Mironov : https://github.com/mirontoli +//! moment.js locale configuration +//! locale : chuvash (cv) +//! author : Anatoly Mironov : https://github.com/mirontoli -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('cv', { - months : "кăрлач_нарăс_пуш_ака_май_çĕртме_утă_çурла_авăн_юпа_чӳк_раштав".split("_"), - monthsShort : "кăр_нар_пуш_ака_май_çĕр_утă_çур_ав_юпа_чӳк_раш".split("_"), - weekdays : "вырсарникун_тунтикун_ытларикун_юнкун_кĕçнерникун_эрнекун_шăматкун".split("_"), - weekdaysShort : "выр_тун_ытл_юн_кĕç_эрн_шăм".split("_"), - weekdaysMin : "вр_тн_ыт_юн_кç_эр_шм".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var cv = moment.defineLocale('cv', { + months : 'кӑрлач_нарӑс_пуш_ака_май_ҫӗртме_утӑ_ҫурла_авӑн_юпа_чӳк_раштав'.split('_'), + monthsShort : 'кӑр_нар_пуш_ака_май_ҫӗр_утӑ_ҫур_авн_юпа_чӳк_раш'.split('_'), + weekdays : 'вырсарникун_тунтикун_ытларикун_юнкун_кӗҫнерникун_эрнекун_шӑматкун'.split('_'), + weekdaysShort : 'выр_тун_ытл_юн_кӗҫ_эрн_шӑм'.split('_'), + weekdaysMin : 'вр_тн_ыт_юн_кҫ_эр_шм'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD-MM-YYYY", - LL : "YYYY [çулхи] MMMM [уйăхĕн] D[-мĕшĕ]", - LLL : "YYYY [çулхи] MMMM [уйăхĕн] D[-мĕшĕ], LT", - LLLL : "dddd, YYYY [çулхи] MMMM [уйăхĕн] D[-мĕшĕ], LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD-MM-YYYY', + LL : 'YYYY [ҫулхи] MMMM [уйӑхӗн] D[-мӗшӗ]', + LLL : 'YYYY [ҫулхи] MMMM [уйӑхӗн] D[-мӗшӗ], HH:mm', + LLLL : 'dddd, YYYY [ҫулхи] MMMM [уйӑхӗн] D[-мӗшӗ], HH:mm' }, calendar : { sameDay: '[Паян] LT [сехетре]', nextDay: '[Ыран] LT [сехетре]', - lastDay: '[Ĕнер] LT [сехетре]', - nextWeek: '[Çитес] dddd LT [сехетре]', - lastWeek: '[Иртнĕ] dddd LT [сехетре]', + lastDay: '[Ӗнер] LT [сехетре]', + nextWeek: '[Ҫитес] dddd LT [сехетре]', + lastWeek: '[Иртнӗ] dddd LT [сехетре]', sameElse: 'L' }, relativeTime : { future : function (output) { - var affix = /сехет$/i.exec(output) ? "рен" : /çул$/i.exec(output) ? "тан" : "ран"; + var affix = /сехет$/i.exec(output) ? 'рен' : /ҫул$/i.exec(output) ? 'тан' : 'ран'; return output + affix; }, - past : "%s каялла", - s : "пĕр-ик çеккунт", - m : "пĕр минут", - mm : "%d минут", - h : "пĕр сехет", - hh : "%d сехет", - d : "пĕр кун", - dd : "%d кун", - M : "пĕр уйăх", - MM : "%d уйăх", - y : "пĕр çул", - yy : "%d çул" + past : '%s каялла', + s : 'пӗр-ик ҫеккунт', + m : 'пӗр минут', + mm : '%d минут', + h : 'пӗр сехет', + hh : '%d сехет', + d : 'пӗр кун', + dd : '%d кун', + M : 'пӗр уйӑх', + MM : '%d уйӑх', + y : 'пӗр ҫул', + yy : '%d ҫул' }, - ordinal : '%d-мĕш', + ordinalParse: /\d{1,2}-мӗш/, + ordinal : '%d-мӗш', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return cv; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/cy.js b/lib/javascripts/moment_locale/cy.js index 6231a52e51..64dfe43f50 100644 --- a/lib/javascripts/moment_locale/cy.js +++ b/lib/javascripts/moment_locale/cy.js @@ -1,29 +1,29 @@ -// moment.js locale configuration -// locale : Welsh (cy) -// author : Robert Allen +//! moment.js locale configuration +//! locale : Welsh (cy) +//! author : Robert Allen -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale("cy", { - months: "Ionawr_Chwefror_Mawrth_Ebrill_Mai_Mehefin_Gorffennaf_Awst_Medi_Hydref_Tachwedd_Rhagfyr".split("_"), - monthsShort: "Ion_Chwe_Maw_Ebr_Mai_Meh_Gor_Aws_Med_Hyd_Tach_Rhag".split("_"), - weekdays: "Dydd Sul_Dydd Llun_Dydd Mawrth_Dydd Mercher_Dydd Iau_Dydd Gwener_Dydd Sadwrn".split("_"), - weekdaysShort: "Sul_Llun_Maw_Mer_Iau_Gwe_Sad".split("_"), - weekdaysMin: "Su_Ll_Ma_Me_Ia_Gw_Sa".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var cy = moment.defineLocale('cy', { + months: 'Ionawr_Chwefror_Mawrth_Ebrill_Mai_Mehefin_Gorffennaf_Awst_Medi_Hydref_Tachwedd_Rhagfyr'.split('_'), + monthsShort: 'Ion_Chwe_Maw_Ebr_Mai_Meh_Gor_Aws_Med_Hyd_Tach_Rhag'.split('_'), + weekdays: 'Dydd Sul_Dydd Llun_Dydd Mawrth_Dydd Mercher_Dydd Iau_Dydd Gwener_Dydd Sadwrn'.split('_'), + weekdaysShort: 'Sul_Llun_Maw_Mer_Iau_Gwe_Sad'.split('_'), + weekdaysMin: 'Su_Ll_Ma_Me_Ia_Gw_Sa'.split('_'), // time formats are the same as en-gb longDateFormat: { - LT: "HH:mm", - L: "DD/MM/YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY LT", - LLLL: "dddd, D MMMM YYYY LT" + LT: 'HH:mm', + LTS : 'HH:mm:ss', + L: 'DD/MM/YYYY', + LL: 'D MMMM YYYY', + LLL: 'D MMMM YYYY HH:mm', + LLLL: 'dddd, D MMMM YYYY HH:mm' }, calendar: { sameDay: '[Heddiw am] LT', @@ -34,20 +34,21 @@ sameElse: 'L' }, relativeTime: { - future: "mewn %s", - past: "%s yn ôl", - s: "ychydig eiliadau", - m: "munud", - mm: "%d munud", - h: "awr", - hh: "%d awr", - d: "diwrnod", - dd: "%d diwrnod", - M: "mis", - MM: "%d mis", - y: "blwyddyn", - yy: "%d flynedd" + future: 'mewn %s', + past: '%s yn ôl', + s: 'ychydig eiliadau', + m: 'munud', + mm: '%d munud', + h: 'awr', + hh: '%d awr', + d: 'diwrnod', + dd: '%d diwrnod', + M: 'mis', + MM: '%d mis', + y: 'blwyddyn', + yy: '%d flynedd' }, + ordinalParse: /\d{1,2}(fed|ain|af|il|ydd|ed|eg)/, // traditional ordinal numbers above 31 are not commonly used in colloquial Welsh ordinal: function (number) { var b = number, @@ -56,7 +57,6 @@ '', 'af', 'il', 'ydd', 'ydd', 'ed', 'ed', 'ed', 'fed', 'fed', 'fed', // 1af to 10fed 'eg', 'fed', 'eg', 'eg', 'fed', 'eg', 'eg', 'fed', 'eg', 'fed' // 11eg to 20fed ]; - if (b > 20) { if (b === 40 || b === 50 || b === 60 || b === 80 || b === 100) { output = 'fed'; // not 30ain, 70ain or 90ain @@ -66,7 +66,6 @@ } else if (b > 0) { output = lookup[b]; } - return number + output; }, week : { @@ -74,4 +73,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return cy; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/da.js b/lib/javascripts/moment_locale/da.js index 9c1c68fab1..70b4c0da43 100644 --- a/lib/javascripts/moment_locale/da.js +++ b/lib/javascripts/moment_locale/da.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : danish (da) -// author : Ulrik Nielsen : https://github.com/mrbase +//! moment.js locale configuration +//! locale : danish (da) +//! author : Ulrik Nielsen : https://github.com/mrbase -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('da', { - months : "januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"), - monthsShort : "jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"), - weekdays : "søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"), - weekdaysShort : "søn_man_tir_ons_tor_fre_lør".split("_"), - weekdaysMin : "sø_ma_ti_on_to_fr_lø".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var da = moment.defineLocale('da', { + months : 'januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december'.split('_'), + monthsShort : 'jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec'.split('_'), + weekdays : 'søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag'.split('_'), + weekdaysShort : 'søn_man_tir_ons_tor_fre_lør'.split('_'), + weekdaysMin : 'sø_ma_ti_on_to_fr_lø'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY LT", - LLLL : "dddd [d.] D. MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY HH:mm', + LLLL : 'dddd [d.] D. MMMM YYYY HH:mm' }, calendar : { sameDay : '[I dag kl.] LT', @@ -33,24 +33,28 @@ sameElse : 'L' }, relativeTime : { - future : "om %s", - past : "%s siden", - s : "få sekunder", - m : "et minut", - mm : "%d minutter", - h : "en time", - hh : "%d timer", - d : "en dag", - dd : "%d dage", - M : "en måned", - MM : "%d måneder", - y : "et år", - yy : "%d år" + future : 'om %s', + past : '%s siden', + s : 'få sekunder', + m : 'et minut', + mm : '%d minutter', + h : 'en time', + hh : '%d timer', + d : 'en dag', + dd : '%d dage', + M : 'en måned', + MM : '%d måneder', + y : 'et år', + yy : '%d år' }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return da; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/de-at.js b/lib/javascripts/moment_locale/de-at.js index 48d1b88135..20da9cf613 100644 --- a/lib/javascripts/moment_locale/de-at.js +++ b/lib/javascripts/moment_locale/de-at.js @@ -1,18 +1,18 @@ -// moment.js locale configuration -// locale : austrian german (de-at) -// author : lluchs : https://github.com/lluchs -// author: Menelion Elensúle: https://github.com/Oire -// author : Martin Groller : https://github.com/MadMG +//! moment.js locale configuration +//! locale : austrian german (de-at) +//! author : lluchs : https://github.com/lluchs +//! author: Menelion Elensúle: https://github.com/Oire +//! author : Martin Groller : https://github.com/MadMG +//! author : Mikolaj Dadela : https://github.com/mik01aj + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function processRelativeTime(number, withoutSuffix, key, isFuture) { var format = { 'm': ['eine Minute', 'einer Minute'], @@ -27,35 +27,36 @@ return withoutSuffix ? format[key][0] : format[key][1]; } - return moment.defineLocale('de-at', { - months : "Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"), - monthsShort : "Jän._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"), - weekdays : "Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"), - weekdaysShort : "So._Mo._Di._Mi._Do._Fr._Sa.".split("_"), - weekdaysMin : "So_Mo_Di_Mi_Do_Fr_Sa".split("_"), + var de_at = moment.defineLocale('de-at', { + months : 'Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember'.split('_'), + monthsShort : 'Jän._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.'.split('_'), + weekdays : 'Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag'.split('_'), + weekdaysShort : 'So._Mo._Di._Mi._Do._Fr._Sa.'.split('_'), + weekdaysMin : 'So_Mo_Di_Mi_Do_Fr_Sa'.split('_'), longDateFormat : { - LT: "HH:mm [Uhr]", - L : "DD.MM.YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY LT", - LLLL : "dddd, D. MMMM YYYY LT" + LT: 'HH:mm', + LTS: 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY HH:mm', + LLLL : 'dddd, D. MMMM YYYY HH:mm' }, calendar : { - sameDay: "[Heute um] LT", - sameElse: "L", - nextDay: '[Morgen um] LT', - nextWeek: 'dddd [um] LT', - lastDay: '[Gestern um] LT', - lastWeek: '[letzten] dddd [um] LT' + sameDay: '[heute um] LT [Uhr]', + sameElse: 'L', + nextDay: '[morgen um] LT [Uhr]', + nextWeek: 'dddd [um] LT [Uhr]', + lastDay: '[gestern um] LT [Uhr]', + lastWeek: '[letzten] dddd [um] LT [Uhr]' }, relativeTime : { - future : "in %s", - past : "vor %s", - s : "ein paar Sekunden", + future : 'in %s', + past : 'vor %s', + s : 'ein paar Sekunden', m : processRelativeTime, - mm : "%d Minuten", + mm : '%d Minuten', h : processRelativeTime, - hh : "%d Stunden", + hh : '%d Stunden', d : processRelativeTime, dd : processRelativeTime, M : processRelativeTime, @@ -63,10 +64,14 @@ y : processRelativeTime, yy : processRelativeTime }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return de_at; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/de.js b/lib/javascripts/moment_locale/de.js index 0c389f92f4..41c81a1af6 100644 --- a/lib/javascripts/moment_locale/de.js +++ b/lib/javascripts/moment_locale/de.js @@ -1,17 +1,17 @@ -// moment.js locale configuration -// locale : german (de) -// author : lluchs : https://github.com/lluchs -// author: Menelion Elensúle: https://github.com/Oire +//! moment.js locale configuration +//! locale : german (de) +//! author : lluchs : https://github.com/lluchs +//! author: Menelion Elensúle: https://github.com/Oire +//! author : Mikolaj Dadela : https://github.com/mik01aj + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function processRelativeTime(number, withoutSuffix, key, isFuture) { var format = { 'm': ['eine Minute', 'einer Minute'], @@ -26,35 +26,36 @@ return withoutSuffix ? format[key][0] : format[key][1]; } - return moment.defineLocale('de', { - months : "Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"), - monthsShort : "Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"), - weekdays : "Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"), - weekdaysShort : "So._Mo._Di._Mi._Do._Fr._Sa.".split("_"), - weekdaysMin : "So_Mo_Di_Mi_Do_Fr_Sa".split("_"), + var de = moment.defineLocale('de', { + months : 'Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember'.split('_'), + monthsShort : 'Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.'.split('_'), + weekdays : 'Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag'.split('_'), + weekdaysShort : 'So._Mo._Di._Mi._Do._Fr._Sa.'.split('_'), + weekdaysMin : 'So_Mo_Di_Mi_Do_Fr_Sa'.split('_'), longDateFormat : { - LT: "HH:mm [Uhr]", - L : "DD.MM.YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY LT", - LLLL : "dddd, D. MMMM YYYY LT" + LT: 'HH:mm', + LTS: 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY HH:mm', + LLLL : 'dddd, D. MMMM YYYY HH:mm' }, calendar : { - sameDay: "[Heute um] LT", - sameElse: "L", - nextDay: '[Morgen um] LT', - nextWeek: 'dddd [um] LT', - lastDay: '[Gestern um] LT', - lastWeek: '[letzten] dddd [um] LT' + sameDay: '[heute um] LT [Uhr]', + sameElse: 'L', + nextDay: '[morgen um] LT [Uhr]', + nextWeek: 'dddd [um] LT [Uhr]', + lastDay: '[gestern um] LT [Uhr]', + lastWeek: '[letzten] dddd [um] LT [Uhr]' }, relativeTime : { - future : "in %s", - past : "vor %s", - s : "ein paar Sekunden", + future : 'in %s', + past : 'vor %s', + s : 'ein paar Sekunden', m : processRelativeTime, - mm : "%d Minuten", + mm : '%d Minuten', h : processRelativeTime, - hh : "%d Stunden", + hh : '%d Stunden', d : processRelativeTime, dd : processRelativeTime, M : processRelativeTime, @@ -62,10 +63,14 @@ y : processRelativeTime, yy : processRelativeTime }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return de; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/dv.js b/lib/javascripts/moment_locale/dv.js new file mode 100644 index 0000000000..5fc59b668b --- /dev/null +++ b/lib/javascripts/moment_locale/dv.js @@ -0,0 +1,99 @@ +//! moment.js locale configuration +//! locale : dhivehi (dv) +//! author : Jawish Hameed : https://github.com/jawish + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var months = [ + 'ޖެނުއަރީ', + 'ފެބްރުއަރީ', + 'މާރިޗު', + 'އޭޕްރީލު', + 'މޭ', + 'ޖޫން', + 'ޖުލައި', + 'އޯގަސްޓު', + 'ސެޕްޓެމްބަރު', + 'އޮކްޓޯބަރު', + 'ނޮވެމްބަރު', + 'ޑިސެމްބަރު' + ], weekdays = [ + 'އާދިއްތަ', + 'ހޯމަ', + 'އަންގާރަ', + 'ބުދަ', + 'ބުރާސްފަތި', + 'ހުކުރު', + 'ހޮނިހިރު' + ]; + + var dv = moment.defineLocale('dv', { + months : months, + monthsShort : months, + weekdays : weekdays, + weekdaysShort : weekdays, + weekdaysMin : 'އާދި_ހޯމަ_އަން_ބުދަ_ބުރާ_ހުކު_ހޮނި'.split('_'), + longDateFormat : { + + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'D/M/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' + }, + meridiemParse: /މކ|މފ/, + isPM : function (input) { + return '' === input; + }, + meridiem : function (hour, minute, isLower) { + if (hour < 12) { + return 'މކ'; + } else { + return 'މފ'; + } + }, + calendar : { + sameDay : '[މިއަދު] LT', + nextDay : '[މާދަމާ] LT', + nextWeek : 'dddd LT', + lastDay : '[އިއްޔެ] LT', + lastWeek : '[ފާއިތުވި] dddd LT', + sameElse : 'L' + }, + relativeTime : { + future : 'ތެރޭގައި %s', + past : 'ކުރިން %s', + s : 'ސިކުންތުކޮޅެއް', + m : 'މިނިޓެއް', + mm : 'މިނިޓު %d', + h : 'ގަޑިއިރެއް', + hh : 'ގަޑިއިރު %d', + d : 'ދުވަހެއް', + dd : 'ދުވަސް %d', + M : 'މަހެއް', + MM : 'މަސް %d', + y : 'އަހަރެއް', + yy : 'އަހަރު %d' + }, + preparse: function (string) { + return string.replace(/،/g, ','); + }, + postformat: function (string) { + return string.replace(/,/g, '،'); + }, + week : { + dow : 7, // Sunday is the first day of the week. + doy : 12 // The week that contains Jan 1st is the first week of the year. + } + }); + + return dv; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/el.js b/lib/javascripts/moment_locale/el.js index 7f31628d36..d86666dcaf 100644 --- a/lib/javascripts/moment_locale/el.js +++ b/lib/javascripts/moment_locale/el.js @@ -1,30 +1,33 @@ -// moment.js locale configuration -// locale : modern greek (el) -// author : Aggelos Karalias : https://github.com/mehiel +//! moment.js locale configuration +//! locale : modern greek (el) +//! author : Aggelos Karalias : https://github.com/mehiel -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + function isFunction(input) { + return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]'; } -}(function (moment) { - return moment.defineLocale('el', { - monthsNominativeEl : "Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος".split("_"), - monthsGenitiveEl : "Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου".split("_"), + + + var el = moment.defineLocale('el', { + monthsNominativeEl : 'Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος'.split('_'), + monthsGenitiveEl : 'Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου'.split('_'), months : function (momentToFormat, format) { - if (/D/.test(format.substring(0, format.indexOf("MMMM")))) { // if there is a day number before 'MMMM' + if (/D/.test(format.substring(0, format.indexOf('MMMM')))) { // if there is a day number before 'MMMM' return this._monthsGenitiveEl[momentToFormat.month()]; } else { return this._monthsNominativeEl[momentToFormat.month()]; } }, - monthsShort : "Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ".split("_"), - weekdays : "Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο".split("_"), - weekdaysShort : "Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ".split("_"), - weekdaysMin : "Κυ_Δε_Τρ_Τε_Πε_Πα_Σα".split("_"), + monthsShort : 'Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ'.split('_'), + weekdays : 'Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο'.split('_'), + weekdaysShort : 'Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ'.split('_'), + weekdaysMin : 'Κυ_Δε_Τρ_Τε_Πε_Πα_Σα'.split('_'), meridiem : function (hours, minutes, isLower) { if (hours > 11) { return isLower ? 'μμ' : 'ΜΜ'; @@ -32,12 +35,17 @@ return isLower ? 'πμ' : 'ΠΜ'; } }, + isPM : function (input) { + return ((input + '').toLowerCase()[0] === 'μ'); + }, + meridiemParse : /[ΠΜ]\.?Μ?\.?/i, longDateFormat : { - LT : "h:mm A", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'h:mm A', + LTS : 'h:mm:ss A', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY h:mm A', + LLLL : 'dddd, D MMMM YYYY h:mm A' }, calendarEl : { sameDay : '[Σήμερα {}] LT', @@ -57,34 +65,34 @@ calendar : function (key, mom) { var output = this._calendarEl[key], hours = mom && mom.hours(); - - if (typeof output === 'function') { + if (isFunction(output)) { output = output.apply(mom); } - - return output.replace("{}", (hours % 12 === 1 ? "στη" : "στις")); + return output.replace('{}', (hours % 12 === 1 ? 'στη' : 'στις')); }, relativeTime : { - future : "σε %s", - past : "%s πριν", - s : "δευτερόλεπτα", - m : "ένα λεπτό", - mm : "%d λεπτά", - h : "μία ώρα", - hh : "%d ώρες", - d : "μία μέρα", - dd : "%d μέρες", - M : "ένας μήνας", - MM : "%d μήνες", - y : "ένας χρόνος", - yy : "%d χρόνια" - }, - ordinal : function (number) { - return number + 'η'; + future : 'σε %s', + past : '%s πριν', + s : 'λίγα δευτερόλεπτα', + m : 'ένα λεπτό', + mm : '%d λεπτά', + h : 'μία ώρα', + hh : '%d ώρες', + d : 'μία μέρα', + dd : '%d μέρες', + M : 'ένας μήνας', + MM : '%d μήνες', + y : 'ένας χρόνος', + yy : '%d χρόνια' }, + ordinalParse: /\d{1,2}η/, + ordinal: '%dη', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4st is the first week of the year. } }); -})); + + return el; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/en-au.js b/lib/javascripts/moment_locale/en-au.js index 852ecc9f03..58608c1834 100644 --- a/lib/javascripts/moment_locale/en-au.js +++ b/lib/javascripts/moment_locale/en-au.js @@ -1,27 +1,27 @@ -// moment.js locale configuration -// locale : australian english (en-au) +//! moment.js locale configuration +//! locale : australian english (en-au) -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('en-au', { - months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), - monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), - weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), - weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), - weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var en_au = moment.defineLocale('en-au', { + months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), longDateFormat : { - LT : "h:mm A", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'h:mm A', + LTS : 'h:mm:ss A', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY h:mm A', + LLLL : 'dddd, D MMMM YYYY h:mm A' }, calendar : { sameDay : '[Today at] LT', @@ -32,20 +32,21 @@ sameElse : 'L' }, relativeTime : { - future : "in %s", - past : "%s ago", - s : "a few seconds", - m : "a minute", - mm : "%d minutes", - h : "an hour", - hh : "%d hours", - d : "a day", - dd : "%d days", - M : "a month", - MM : "%d months", - y : "a year", - yy : "%d years" + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' }, + ordinalParse: /\d{1,2}(st|nd|rd|th)/, ordinal : function (number) { var b = number % 10, output = (~~(number % 100 / 10) === 1) ? 'th' : @@ -59,4 +60,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return en_au; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/en-ca.js b/lib/javascripts/moment_locale/en-ca.js index ce253a8335..f0ee032e62 100644 --- a/lib/javascripts/moment_locale/en-ca.js +++ b/lib/javascripts/moment_locale/en-ca.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : canadian english (en-ca) -// author : Jonathan Abourbih : https://github.com/jonbca +//! moment.js locale configuration +//! locale : canadian english (en-ca) +//! author : Jonathan Abourbih : https://github.com/jonbca -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('en-ca', { - months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), - monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), - weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), - weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), - weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var en_ca = moment.defineLocale('en-ca', { + months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), longDateFormat : { - LT : "h:mm A", - L : "YYYY-MM-DD", - LL : "D MMMM, YYYY", - LLL : "D MMMM, YYYY LT", - LLLL : "dddd, D MMMM, YYYY LT" + LT : 'h:mm A', + LTS : 'h:mm:ss A', + L : 'YYYY-MM-DD', + LL : 'D MMMM, YYYY', + LLL : 'D MMMM, YYYY h:mm A', + LLLL : 'dddd, D MMMM, YYYY h:mm A' }, calendar : { sameDay : '[Today at] LT', @@ -33,20 +33,21 @@ sameElse : 'L' }, relativeTime : { - future : "in %s", - past : "%s ago", - s : "a few seconds", - m : "a minute", - mm : "%d minutes", - h : "an hour", - hh : "%d hours", - d : "a day", - dd : "%d days", - M : "a month", - MM : "%d months", - y : "a year", - yy : "%d years" + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' }, + ordinalParse: /\d{1,2}(st|nd|rd|th)/, ordinal : function (number) { var b = number % 10, output = (~~(number % 100 / 10) === 1) ? 'th' : @@ -56,4 +57,7 @@ return number + output; } }); -})); + + return en_ca; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/en-gb.js b/lib/javascripts/moment_locale/en-gb.js index 14ccbab3ed..47b2c209b7 100644 --- a/lib/javascripts/moment_locale/en-gb.js +++ b/lib/javascripts/moment_locale/en-gb.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : great britain english (en-gb) -// author : Chris Gedrim : https://github.com/chrisgedrim +//! moment.js locale configuration +//! locale : great britain english (en-gb) +//! author : Chris Gedrim : https://github.com/chrisgedrim -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('en-gb', { - months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), - monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), - weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), - weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), - weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var en_gb = moment.defineLocale('en-gb', { + months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' }, calendar : { sameDay : '[Today at] LT', @@ -33,20 +33,21 @@ sameElse : 'L' }, relativeTime : { - future : "in %s", - past : "%s ago", - s : "a few seconds", - m : "a minute", - mm : "%d minutes", - h : "an hour", - hh : "%d hours", - d : "a day", - dd : "%d days", - M : "a month", - MM : "%d months", - y : "a year", - yy : "%d years" + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' }, + ordinalParse: /\d{1,2}(st|nd|rd|th)/, ordinal : function (number) { var b = number % 10, output = (~~(number % 100 / 10) === 1) ? 'th' : @@ -60,4 +61,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return en_gb; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/en-ie.js b/lib/javascripts/moment_locale/en-ie.js new file mode 100644 index 0000000000..c0ff10c405 --- /dev/null +++ b/lib/javascripts/moment_locale/en-ie.js @@ -0,0 +1,67 @@ +//! moment.js locale configuration +//! locale : Irish english (en-ie) +//! author : Chris Cartlidge : https://github.com/chriscartlidge + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var en_ie = moment.defineLocale('en-ie', { + months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + longDateFormat : { + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD-MM-YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' + }, + calendar : { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }, + relativeTime : { + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' + }, + ordinalParse: /\d{1,2}(st|nd|rd|th)/, + ordinal : function (number) { + var b = number % 10, + output = (~~(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } + }); + + return en_ie; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/en-nz.js b/lib/javascripts/moment_locale/en-nz.js new file mode 100644 index 0000000000..14a50ea4bd --- /dev/null +++ b/lib/javascripts/moment_locale/en-nz.js @@ -0,0 +1,66 @@ +//! moment.js locale configuration +//! locale : New Zealand english (en-nz) + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var en_nz = moment.defineLocale('en-nz', { + months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + longDateFormat : { + LT : 'h:mm A', + LTS : 'h:mm:ss A', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY h:mm A', + LLLL : 'dddd, D MMMM YYYY h:mm A' + }, + calendar : { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }, + relativeTime : { + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' + }, + ordinalParse: /\d{1,2}(st|nd|rd|th)/, + ordinal : function (number) { + var b = number % 10, + output = (~~(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } + }); + + return en_nz; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/eo.js b/lib/javascripts/moment_locale/eo.js index 318385b950..92772dffa4 100644 --- a/lib/javascripts/moment_locale/eo.js +++ b/lib/javascripts/moment_locale/eo.js @@ -1,30 +1,34 @@ -// moment.js locale configuration -// locale : esperanto (eo) -// author : Colin Dean : https://github.com/colindean -// komento: Mi estas malcerta se mi korekte traktis akuzativojn en tiu traduko. -// Se ne, bonvolu korekti kaj avizi min por ke mi povas lerni! +//! moment.js locale configuration +//! locale : esperanto (eo) +//! author : Colin Dean : https://github.com/colindean +//! komento: Mi estas malcerta se mi korekte traktis akuzativojn en tiu traduko. +//! Se ne, bonvolu korekti kaj avizi min por ke mi povas lerni! -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('eo', { - months : "januaro_februaro_marto_aprilo_majo_junio_julio_aŭgusto_septembro_oktobro_novembro_decembro".split("_"), - monthsShort : "jan_feb_mar_apr_maj_jun_jul_aŭg_sep_okt_nov_dec".split("_"), - weekdays : "Dimanĉo_Lundo_Mardo_Merkredo_Ĵaŭdo_Vendredo_Sabato".split("_"), - weekdaysShort : "Dim_Lun_Mard_Merk_Ĵaŭ_Ven_Sab".split("_"), - weekdaysMin : "Di_Lu_Ma_Me_Ĵa_Ve_Sa".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var eo = moment.defineLocale('eo', { + months : 'januaro_februaro_marto_aprilo_majo_junio_julio_aŭgusto_septembro_oktobro_novembro_decembro'.split('_'), + monthsShort : 'jan_feb_mar_apr_maj_jun_jul_aŭg_sep_okt_nov_dec'.split('_'), + weekdays : 'Dimanĉo_Lundo_Mardo_Merkredo_Ĵaŭdo_Vendredo_Sabato'.split('_'), + weekdaysShort : 'Dim_Lun_Mard_Merk_Ĵaŭ_Ven_Sab'.split('_'), + weekdaysMin : 'Di_Lu_Ma_Me_Ĵa_Ve_Sa'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "YYYY-MM-DD", - LL : "D[-an de] MMMM, YYYY", - LLL : "D[-an de] MMMM, YYYY LT", - LLLL : "dddd, [la] D[-an de] MMMM, YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'YYYY-MM-DD', + LL : 'D[-an de] MMMM, YYYY', + LLL : 'D[-an de] MMMM, YYYY HH:mm', + LLLL : 'dddd, [la] D[-an de] MMMM, YYYY HH:mm' + }, + meridiemParse: /[ap]\.t\.m/i, + isPM: function (input) { + return input.charAt(0).toLowerCase() === 'p'; }, meridiem : function (hours, minutes, isLower) { if (hours > 11) { @@ -42,24 +46,28 @@ sameElse : 'L' }, relativeTime : { - future : "je %s", - past : "antaŭ %s", - s : "sekundoj", - m : "minuto", - mm : "%d minutoj", - h : "horo", - hh : "%d horoj", - d : "tago",//ne 'diurno', ĉar estas uzita por proksimumo - dd : "%d tagoj", - M : "monato", - MM : "%d monatoj", - y : "jaro", - yy : "%d jaroj" + future : 'je %s', + past : 'antaŭ %s', + s : 'sekundoj', + m : 'minuto', + mm : '%d minutoj', + h : 'horo', + hh : '%d horoj', + d : 'tago',//ne 'diurno', ĉar estas uzita por proksimumo + dd : '%d tagoj', + M : 'monato', + MM : '%d monatoj', + y : 'jaro', + yy : '%d jaroj' }, - ordinal : "%da", + ordinalParse: /\d{1,2}a/, + ordinal : '%da', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return eo; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/es.js b/lib/javascripts/moment_locale/es.js index ed0b564450..efb51a31d0 100644 --- a/lib/javascripts/moment_locale/es.js +++ b/lib/javascripts/moment_locale/es.js @@ -1,21 +1,20 @@ -// moment.js locale configuration -// locale : spanish (es) -// author : Julio Napurí : https://github.com/julionc +//! moment.js locale configuration +//! locale : spanish (es) +//! author : Julio Napurí : https://github.com/julionc -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - var monthsShortDot = "ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"), - monthsShort = "ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"); +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; - return moment.defineLocale('es', { - months : "enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"), + + var monthsShortDot = 'ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.'.split('_'), + monthsShort = 'ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic'.split('_'); + + var es = moment.defineLocale('es', { + months : 'enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre'.split('_'), monthsShort : function (m, format) { if (/-MMM-/.test(format)) { return monthsShort[m.month()]; @@ -23,15 +22,16 @@ return monthsShortDot[m.month()]; } }, - weekdays : "domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"), - weekdaysShort : "dom._lun._mar._mié._jue._vie._sáb.".split("_"), - weekdaysMin : "Do_Lu_Ma_Mi_Ju_Vi_Sá".split("_"), + weekdays : 'domingo_lunes_martes_miércoles_jueves_viernes_sábado'.split('_'), + weekdaysShort : 'dom._lun._mar._mié._jue._vie._sáb.'.split('_'), + weekdaysMin : 'do_lu_ma_mi_ju_vi_sá'.split('_'), longDateFormat : { - LT : "H:mm", - L : "DD/MM/YYYY", - LL : "D [de] MMMM [del] YYYY", - LLL : "D [de] MMMM [del] YYYY LT", - LLLL : "dddd, D [de] MMMM [del] YYYY LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D [de] MMMM [de] YYYY', + LLL : 'D [de] MMMM [de] YYYY H:mm', + LLLL : 'dddd, D [de] MMMM [de] YYYY H:mm' }, calendar : { sameDay : function () { @@ -52,24 +52,28 @@ sameElse : 'L' }, relativeTime : { - future : "en %s", - past : "hace %s", - s : "unos segundos", - m : "un minuto", - mm : "%d minutos", - h : "una hora", - hh : "%d horas", - d : "un día", - dd : "%d días", - M : "un mes", - MM : "%d meses", - y : "un año", - yy : "%d años" + future : 'en %s', + past : 'hace %s', + s : 'unos segundos', + m : 'un minuto', + mm : '%d minutos', + h : 'una hora', + hh : '%d horas', + d : 'un día', + dd : '%d días', + M : 'un mes', + MM : '%d meses', + y : 'un año', + yy : '%d años' }, + ordinalParse : /\d{1,2}º/, ordinal : '%dº', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return es; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/et.js b/lib/javascripts/moment_locale/et.js index 2241529d23..09043bfad6 100644 --- a/lib/javascripts/moment_locale/et.js +++ b/lib/javascripts/moment_locale/et.js @@ -1,17 +1,16 @@ -// moment.js locale configuration -// locale : estonian (et) -// author : Henry Kehlmann : https://github.com/madhenry -// improvements : Illimar Tambek : https://github.com/ragulka +//! moment.js locale configuration +//! locale : estonian (et) +//! author : Henry Kehlmann : https://github.com/madhenry +//! improvements : Illimar Tambek : https://github.com/ragulka + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function processRelativeTime(number, withoutSuffix, key, isFuture) { var format = { 's' : ['mõne sekundi', 'mõni sekund', 'paar sekundit'], @@ -31,18 +30,19 @@ return isFuture ? format[key][0] : format[key][1]; } - return moment.defineLocale('et', { - months : "jaanuar_veebruar_märts_aprill_mai_juuni_juuli_august_september_oktoober_november_detsember".split("_"), - monthsShort : "jaan_veebr_märts_apr_mai_juuni_juuli_aug_sept_okt_nov_dets".split("_"), - weekdays : "pühapäev_esmaspäev_teisipäev_kolmapäev_neljapäev_reede_laupäev".split("_"), - weekdaysShort : "P_E_T_K_N_R_L".split("_"), - weekdaysMin : "P_E_T_K_N_R_L".split("_"), + var et = moment.defineLocale('et', { + months : 'jaanuar_veebruar_märts_aprill_mai_juuni_juuli_august_september_oktoober_november_detsember'.split('_'), + monthsShort : 'jaan_veebr_märts_apr_mai_juuni_juuli_aug_sept_okt_nov_dets'.split('_'), + weekdays : 'pühapäev_esmaspäev_teisipäev_kolmapäev_neljapäev_reede_laupäev'.split('_'), + weekdaysShort : 'P_E_T_K_N_R_L'.split('_'), + weekdaysMin : 'P_E_T_K_N_R_L'.split('_'), longDateFormat : { - LT : "H:mm", - L : "DD.MM.YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY LT", - LLLL : "dddd, D. MMMM YYYY LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY H:mm', + LLLL : 'dddd, D. MMMM YYYY H:mm' }, calendar : { sameDay : '[Täna,] LT', @@ -53,8 +53,8 @@ sameElse : 'L' }, relativeTime : { - future : "%s pärast", - past : "%s tagasi", + future : '%s pärast', + past : '%s tagasi', s : processRelativeTime, m : processRelativeTime, mm : processRelativeTime, @@ -67,10 +67,14 @@ y : processRelativeTime, yy : processRelativeTime }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return et; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/eu.js b/lib/javascripts/moment_locale/eu.js index fe2dddb7bf..52db117692 100644 --- a/lib/javascripts/moment_locale/eu.js +++ b/lib/javascripts/moment_locale/eu.js @@ -1,32 +1,32 @@ -// moment.js locale configuration -// locale : euskara (eu) -// author : Eneko Illarramendi : https://github.com/eillarra +//! moment.js locale configuration +//! locale : euskara (eu) +//! author : Eneko Illarramendi : https://github.com/eillarra -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('eu', { - months : "urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua".split("_"), - monthsShort : "urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.".split("_"), - weekdays : "igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata".split("_"), - weekdaysShort : "ig._al._ar._az._og._ol._lr.".split("_"), - weekdaysMin : "ig_al_ar_az_og_ol_lr".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var eu = moment.defineLocale('eu', { + months : 'urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua'.split('_'), + monthsShort : 'urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.'.split('_'), + weekdays : 'igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata'.split('_'), + weekdaysShort : 'ig._al._ar._az._og._ol._lr.'.split('_'), + weekdaysMin : 'ig_al_ar_az_og_ol_lr'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "YYYY-MM-DD", - LL : "YYYY[ko] MMMM[ren] D[a]", - LLL : "YYYY[ko] MMMM[ren] D[a] LT", - LLLL : "dddd, YYYY[ko] MMMM[ren] D[a] LT", - l : "YYYY-M-D", - ll : "YYYY[ko] MMM D[a]", - lll : "YYYY[ko] MMM D[a] LT", - llll : "ddd, YYYY[ko] MMM D[a] LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'YYYY-MM-DD', + LL : 'YYYY[ko] MMMM[ren] D[a]', + LLL : 'YYYY[ko] MMMM[ren] D[a] HH:mm', + LLLL : 'dddd, YYYY[ko] MMMM[ren] D[a] HH:mm', + l : 'YYYY-M-D', + ll : 'YYYY[ko] MMM D[a]', + lll : 'YYYY[ko] MMM D[a] HH:mm', + llll : 'ddd, YYYY[ko] MMM D[a] HH:mm' }, calendar : { sameDay : '[gaur] LT[etan]', @@ -37,24 +37,28 @@ sameElse : 'L' }, relativeTime : { - future : "%s barru", - past : "duela %s", - s : "segundo batzuk", - m : "minutu bat", - mm : "%d minutu", - h : "ordu bat", - hh : "%d ordu", - d : "egun bat", - dd : "%d egun", - M : "hilabete bat", - MM : "%d hilabete", - y : "urte bat", - yy : "%d urte" + future : '%s barru', + past : 'duela %s', + s : 'segundo batzuk', + m : 'minutu bat', + mm : '%d minutu', + h : 'ordu bat', + hh : '%d ordu', + d : 'egun bat', + dd : '%d egun', + M : 'hilabete bat', + MM : '%d hilabete', + y : 'urte bat', + yy : '%d urte' }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return eu; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/fa.js b/lib/javascripts/moment_locale/fa.js index 68af5193fd..de40e6f937 100644 --- a/lib/javascripts/moment_locale/fa.js +++ b/lib/javascripts/moment_locale/fa.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : Persian (fa) -// author : Ebrahim Byagowi : https://github.com/ebraminio +//! moment.js locale configuration +//! locale : Persian (fa) +//! author : Ebrahim Byagowi : https://github.com/ebraminio + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var symbolMap = { '1': '۱', '2': '۲', @@ -35,7 +34,7 @@ '۰': '0' }; - return moment.defineLocale('fa', { + var fa = moment.defineLocale('fa', { months : 'ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر'.split('_'), monthsShort : 'ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر'.split('_'), weekdays : 'یک\u200cشنبه_دوشنبه_سه\u200cشنبه_چهارشنبه_پنج\u200cشنبه_جمعه_شنبه'.split('_'), @@ -43,16 +42,21 @@ weekdaysMin : 'ی_د_س_چ_پ_ج_ش'.split('_'), longDateFormat : { LT : 'HH:mm', + LTS : 'HH:mm:ss', L : 'DD/MM/YYYY', LL : 'D MMMM YYYY', - LLL : 'D MMMM YYYY LT', - LLLL : 'dddd, D MMMM YYYY LT' + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' + }, + meridiemParse: /قبل از ظهر|بعد از ظهر/, + isPM: function (input) { + return /بعد از ظهر/.test(input); }, meridiem : function (hour, minute, isLower) { if (hour < 12) { - return "قبل از ظهر"; + return 'قبل از ظهر'; } else { - return "بعد از ظهر"; + return 'بعد از ظهر'; } }, calendar : { @@ -88,10 +92,14 @@ return symbolMap[match]; }).replace(/,/g, '،'); }, + ordinalParse: /\d{1,2}م/, ordinal : '%dم', week : { dow : 6, // Saturday is the first day of the week. doy : 12 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return fa; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/fi.js b/lib/javascripts/moment_locale/fi.js index 2afc5e89ac..4f9161bdd3 100644 --- a/lib/javascripts/moment_locale/fi.js +++ b/lib/javascripts/moment_locale/fi.js @@ -1,24 +1,22 @@ -// moment.js locale configuration -// locale : finnish (fi) -// author : Tarmo Aidantausta : https://github.com/bleadof +//! moment.js locale configuration +//! locale : finnish (fi) +//! author : Tarmo Aidantausta : https://github.com/bleadof + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var numbersPast = 'nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän'.split(' '), numbersFuture = [ 'nolla', 'yhden', 'kahden', 'kolmen', 'neljän', 'viiden', 'kuuden', numbersPast[7], numbersPast[8], numbersPast[9] ]; - function translate(number, withoutSuffix, key, isFuture) { - var result = ""; + var result = ''; switch (key) { case 's': return isFuture ? 'muutaman sekunnin' : 'muutama sekunti'; @@ -48,30 +46,30 @@ result = isFuture ? 'vuoden' : 'vuotta'; break; } - result = verbalNumber(number, isFuture) + " " + result; + result = verbalNumber(number, isFuture) + ' ' + result; return result; } - function verbalNumber(number, isFuture) { return number < 10 ? (isFuture ? numbersFuture[number] : numbersPast[number]) : number; } - return moment.defineLocale('fi', { - months : "tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"), - monthsShort : "tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"), - weekdays : "sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"), - weekdaysShort : "su_ma_ti_ke_to_pe_la".split("_"), - weekdaysMin : "su_ma_ti_ke_to_pe_la".split("_"), + var fi = moment.defineLocale('fi', { + months : 'tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu'.split('_'), + monthsShort : 'tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu'.split('_'), + weekdays : 'sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai'.split('_'), + weekdaysShort : 'su_ma_ti_ke_to_pe_la'.split('_'), + weekdaysMin : 'su_ma_ti_ke_to_pe_la'.split('_'), longDateFormat : { - LT : "HH.mm", - L : "DD.MM.YYYY", - LL : "Do MMMM[ta] YYYY", - LLL : "Do MMMM[ta] YYYY, [klo] LT", - LLLL : "dddd, Do MMMM[ta] YYYY, [klo] LT", - l : "D.M.YYYY", - ll : "Do MMM YYYY", - lll : "Do MMM YYYY, [klo] LT", - llll : "ddd, Do MMM YYYY, [klo] LT" + LT : 'HH.mm', + LTS : 'HH.mm.ss', + L : 'DD.MM.YYYY', + LL : 'Do MMMM[ta] YYYY', + LLL : 'Do MMMM[ta] YYYY, [klo] HH.mm', + LLLL : 'dddd, Do MMMM[ta] YYYY, [klo] HH.mm', + l : 'D.M.YYYY', + ll : 'Do MMM YYYY', + lll : 'Do MMM YYYY, [klo] HH.mm', + llll : 'ddd, Do MMM YYYY, [klo] HH.mm' }, calendar : { sameDay : '[tänään] [klo] LT', @@ -82,8 +80,8 @@ sameElse : 'L' }, relativeTime : { - future : "%s päästä", - past : "%s sitten", + future : '%s päästä', + past : '%s sitten', s : translate, m : translate, mm : translate, @@ -96,10 +94,14 @@ y : translate, yy : translate }, - ordinal : "%d.", + ordinalParse: /\d{1,2}\./, + ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return fi; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/fo.js b/lib/javascripts/moment_locale/fo.js index cdc9eda164..460b6cd81c 100644 --- a/lib/javascripts/moment_locale/fo.js +++ b/lib/javascripts/moment_locale/fo.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : faroese (fo) -// author : Ragnar Johannesen : https://github.com/ragnar123 +//! moment.js locale configuration +//! locale : faroese (fo) +//! author : Ragnar Johannesen : https://github.com/ragnar123 -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('fo', { - months : "januar_februar_mars_apríl_mai_juni_juli_august_september_oktober_november_desember".split("_"), - monthsShort : "jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"), - weekdays : "sunnudagur_mánadagur_týsdagur_mikudagur_hósdagur_fríggjadagur_leygardagur".split("_"), - weekdaysShort : "sun_mán_týs_mik_hós_frí_ley".split("_"), - weekdaysMin : "su_má_tý_mi_hó_fr_le".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var fo = moment.defineLocale('fo', { + months : 'januar_februar_mars_apríl_mai_juni_juli_august_september_oktober_november_desember'.split('_'), + monthsShort : 'jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des'.split('_'), + weekdays : 'sunnudagur_mánadagur_týsdagur_mikudagur_hósdagur_fríggjadagur_leygardagur'.split('_'), + weekdaysShort : 'sun_mán_týs_mik_hós_frí_ley'.split('_'), + weekdaysMin : 'su_má_tý_mi_hó_fr_le'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D. MMMM, YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D. MMMM, YYYY HH:mm' }, calendar : { sameDay : '[Í dag kl.] LT', @@ -33,24 +33,28 @@ sameElse : 'L' }, relativeTime : { - future : "um %s", - past : "%s síðani", - s : "fá sekund", - m : "ein minutt", - mm : "%d minuttir", - h : "ein tími", - hh : "%d tímar", - d : "ein dagur", - dd : "%d dagar", - M : "ein mánaði", - MM : "%d mánaðir", - y : "eitt ár", - yy : "%d ár" + future : 'um %s', + past : '%s síðani', + s : 'fá sekund', + m : 'ein minutt', + mm : '%d minuttir', + h : 'ein tími', + hh : '%d tímar', + d : 'ein dagur', + dd : '%d dagar', + M : 'ein mánaði', + MM : '%d mánaðir', + y : 'eitt ár', + yy : '%d ár' }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return fo; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/fr-ca.js b/lib/javascripts/moment_locale/fr-ca.js index 714b11b2bf..f15ec8dc07 100644 --- a/lib/javascripts/moment_locale/fr-ca.js +++ b/lib/javascripts/moment_locale/fr-ca.js @@ -1,31 +1,31 @@ -// moment.js locale configuration -// locale : canadian french (fr-ca) -// author : Jonathan Abourbih : https://github.com/jonbca +//! moment.js locale configuration +//! locale : canadian french (fr-ca) +//! author : Jonathan Abourbih : https://github.com/jonbca -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('fr-ca', { - months : "janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"), - monthsShort : "janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"), - weekdays : "dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"), - weekdaysShort : "dim._lun._mar._mer._jeu._ven._sam.".split("_"), - weekdaysMin : "Di_Lu_Ma_Me_Je_Ve_Sa".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var fr_ca = moment.defineLocale('fr-ca', { + months : 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_'), + monthsShort : 'janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.'.split('_'), + weekdays : 'dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi'.split('_'), + weekdaysShort : 'dim._lun._mar._mer._jeu._ven._sam.'.split('_'), + weekdaysMin : 'Di_Lu_Ma_Me_Je_Ve_Sa'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "YYYY-MM-DD", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'YYYY-MM-DD', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' }, calendar : { - sameDay: "[Aujourd'hui à] LT", + sameDay: '[Aujourd\'hui à] LT', nextDay: '[Demain à] LT', nextWeek: 'dddd [à] LT', lastDay: '[Hier à] LT', @@ -33,22 +33,26 @@ sameElse: 'L' }, relativeTime : { - future : "dans %s", - past : "il y a %s", - s : "quelques secondes", - m : "une minute", - mm : "%d minutes", - h : "une heure", - hh : "%d heures", - d : "un jour", - dd : "%d jours", - M : "un mois", - MM : "%d mois", - y : "un an", - yy : "%d ans" + future : 'dans %s', + past : 'il y a %s', + s : 'quelques secondes', + m : 'une minute', + mm : '%d minutes', + h : 'une heure', + hh : '%d heures', + d : 'un jour', + dd : '%d jours', + M : 'un mois', + MM : '%d mois', + y : 'un an', + yy : '%d ans' }, + ordinalParse: /\d{1,2}(er|e)/, ordinal : function (number) { - return number + (number === 1 ? 'er' : ''); + return number + (number === 1 ? 'er' : 'e'); } }); -})); + + return fr_ca; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/fr-ch.js b/lib/javascripts/moment_locale/fr-ch.js new file mode 100644 index 0000000000..9503d80f7d --- /dev/null +++ b/lib/javascripts/moment_locale/fr-ch.js @@ -0,0 +1,62 @@ +//! moment.js locale configuration +//! locale : swiss french (fr) +//! author : Gaspard Bucher : https://github.com/gaspard + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var fr_ch = moment.defineLocale('fr-ch', { + months : 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_'), + monthsShort : 'janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.'.split('_'), + weekdays : 'dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi'.split('_'), + weekdaysShort : 'dim._lun._mar._mer._jeu._ven._sam.'.split('_'), + weekdaysMin : 'Di_Lu_Ma_Me_Je_Ve_Sa'.split('_'), + longDateFormat : { + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' + }, + calendar : { + sameDay: '[Aujourd\'hui à] LT', + nextDay: '[Demain à] LT', + nextWeek: 'dddd [à] LT', + lastDay: '[Hier à] LT', + lastWeek: 'dddd [dernier à] LT', + sameElse: 'L' + }, + relativeTime : { + future : 'dans %s', + past : 'il y a %s', + s : 'quelques secondes', + m : 'une minute', + mm : '%d minutes', + h : 'une heure', + hh : '%d heures', + d : 'un jour', + dd : '%d jours', + M : 'un mois', + MM : '%d mois', + y : 'un an', + yy : '%d ans' + }, + ordinalParse: /\d{1,2}(er|e)/, + ordinal : function (number) { + return number + (number === 1 ? 'er' : 'e'); + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } + }); + + return fr_ch; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/fr.js b/lib/javascripts/moment_locale/fr.js index 106ab11bee..8ef95c9fb6 100644 --- a/lib/javascripts/moment_locale/fr.js +++ b/lib/javascripts/moment_locale/fr.js @@ -1,31 +1,31 @@ -// moment.js locale configuration -// locale : french (fr) -// author : John Fischer : https://github.com/jfroffice +//! moment.js locale configuration +//! locale : french (fr) +//! author : John Fischer : https://github.com/jfroffice -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('fr', { - months : "janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"), - monthsShort : "janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"), - weekdays : "dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"), - weekdaysShort : "dim._lun._mar._mer._jeu._ven._sam.".split("_"), - weekdaysMin : "Di_Lu_Ma_Me_Je_Ve_Sa".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var fr = moment.defineLocale('fr', { + months : 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_'), + monthsShort : 'janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.'.split('_'), + weekdays : 'dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi'.split('_'), + weekdaysShort : 'dim._lun._mar._mer._jeu._ven._sam.'.split('_'), + weekdaysMin : 'Di_Lu_Ma_Me_Je_Ve_Sa'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' }, calendar : { - sameDay: "[Aujourd'hui à] LT", + sameDay: '[Aujourd\'hui à] LT', nextDay: '[Demain à] LT', nextWeek: 'dddd [à] LT', lastDay: '[Hier à] LT', @@ -33,20 +33,21 @@ sameElse: 'L' }, relativeTime : { - future : "dans %s", - past : "il y a %s", - s : "quelques secondes", - m : "une minute", - mm : "%d minutes", - h : "une heure", - hh : "%d heures", - d : "un jour", - dd : "%d jours", - M : "un mois", - MM : "%d mois", - y : "un an", - yy : "%d ans" + future : 'dans %s', + past : 'il y a %s', + s : 'quelques secondes', + m : 'une minute', + mm : '%d minutes', + h : 'une heure', + hh : '%d heures', + d : 'un jour', + dd : '%d jours', + M : 'un mois', + MM : '%d mois', + y : 'un an', + yy : '%d ans' }, + ordinalParse: /\d{1,2}(er|)/, ordinal : function (number) { return number + (number === 1 ? 'er' : ''); }, @@ -55,4 +56,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return fr; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/fy.js b/lib/javascripts/moment_locale/fy.js new file mode 100644 index 0000000000..d1b709c122 --- /dev/null +++ b/lib/javascripts/moment_locale/fy.js @@ -0,0 +1,71 @@ +//! moment.js locale configuration +//! locale : frisian (fy) +//! author : Robin van der Vliet : https://github.com/robin0van0der0v + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var monthsShortWithDots = 'jan._feb._mrt._apr._mai_jun._jul._aug._sep._okt._nov._des.'.split('_'), + monthsShortWithoutDots = 'jan_feb_mrt_apr_mai_jun_jul_aug_sep_okt_nov_des'.split('_'); + + var fy = moment.defineLocale('fy', { + months : 'jannewaris_febrewaris_maart_april_maaie_juny_july_augustus_septimber_oktober_novimber_desimber'.split('_'), + monthsShort : function (m, format) { + if (/-MMM-/.test(format)) { + return monthsShortWithoutDots[m.month()]; + } else { + return monthsShortWithDots[m.month()]; + } + }, + weekdays : 'snein_moandei_tiisdei_woansdei_tongersdei_freed_sneon'.split('_'), + weekdaysShort : 'si._mo._ti._wo._to._fr._so.'.split('_'), + weekdaysMin : 'Si_Mo_Ti_Wo_To_Fr_So'.split('_'), + longDateFormat : { + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD-MM-YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' + }, + calendar : { + sameDay: '[hjoed om] LT', + nextDay: '[moarn om] LT', + nextWeek: 'dddd [om] LT', + lastDay: '[juster om] LT', + lastWeek: '[ôfrûne] dddd [om] LT', + sameElse: 'L' + }, + relativeTime : { + future : 'oer %s', + past : '%s lyn', + s : 'in pear sekonden', + m : 'ien minút', + mm : '%d minuten', + h : 'ien oere', + hh : '%d oeren', + d : 'ien dei', + dd : '%d dagen', + M : 'ien moanne', + MM : '%d moannen', + y : 'ien jier', + yy : '%d jierren' + }, + ordinalParse: /\d{1,2}(ste|de)/, + ordinal : function (number) { + return number + ((number === 1 || number === 8 || number >= 20) ? 'ste' : 'de'); + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } + }); + + return fy; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/gd.js b/lib/javascripts/moment_locale/gd.js new file mode 100644 index 0000000000..578e5674b9 --- /dev/null +++ b/lib/javascripts/moment_locale/gd.js @@ -0,0 +1,76 @@ +//! moment.js locale configuration +//! locale : great britain scottish gealic (gd) +//! author : Jon Ashdown : https://github.com/jonashdown + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var months = [ + 'Am Faoilleach', 'An Gearran', 'Am Màrt', 'An Giblean', 'An Cèitean', 'An t-Ògmhios', 'An t-Iuchar', 'An Lùnastal', 'An t-Sultain', 'An Dàmhair', 'An t-Samhain', 'An Dùbhlachd' + ]; + + var monthsShort = ['Faoi', 'Gear', 'Màrt', 'Gibl', 'Cèit', 'Ògmh', 'Iuch', 'Lùn', 'Sult', 'Dàmh', 'Samh', 'Dùbh']; + + var weekdays = ['Didòmhnaich', 'Diluain', 'Dimàirt', 'Diciadain', 'Diardaoin', 'Dihaoine', 'Disathairne']; + + var weekdaysShort = ['Did', 'Dil', 'Dim', 'Dic', 'Dia', 'Dih', 'Dis']; + + var weekdaysMin = ['Dò', 'Lu', 'Mà', 'Ci', 'Ar', 'Ha', 'Sa']; + + var gd = moment.defineLocale('gd', { + months : months, + monthsShort : monthsShort, + monthsParseExact : true, + weekdays : weekdays, + weekdaysShort : weekdaysShort, + weekdaysMin : weekdaysMin, + longDateFormat : { + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' + }, + calendar : { + sameDay : '[An-diugh aig] LT', + nextDay : '[A-màireach aig] LT', + nextWeek : 'dddd [aig] LT', + lastDay : '[An-dè aig] LT', + lastWeek : 'dddd [seo chaidh] [aig] LT', + sameElse : 'L' + }, + relativeTime : { + future : 'ann an %s', + past : 'bho chionn %s', + s : 'beagan diogan', + m : 'mionaid', + mm : '%d mionaidean', + h : 'uair', + hh : '%d uairean', + d : 'latha', + dd : '%d latha', + M : 'mìos', + MM : '%d mìosan', + y : 'bliadhna', + yy : '%d bliadhna' + }, + ordinalParse : /\d{1,2}(d|na|mh)/, + ordinal : function (number) { + var output = number === 1 ? 'd' : number % 10 === 2 ? 'na' : 'mh'; + return number + output; + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } + }); + + return gd; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/gl.js b/lib/javascripts/moment_locale/gl.js index e82065f33d..ef8e7043b8 100644 --- a/lib/javascripts/moment_locale/gl.js +++ b/lib/javascripts/moment_locale/gl.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : galician (gl) -// author : Juan G. Hurtado : https://github.com/juanghurtado +//! moment.js locale configuration +//! locale : galician (gl) +//! author : Juan G. Hurtado : https://github.com/juanghurtado -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('gl', { - months : "Xaneiro_Febreiro_Marzo_Abril_Maio_Xuño_Xullo_Agosto_Setembro_Outubro_Novembro_Decembro".split("_"), - monthsShort : "Xan._Feb._Mar._Abr._Mai._Xuñ._Xul._Ago._Set._Out._Nov._Dec.".split("_"), - weekdays : "Domingo_Luns_Martes_Mércores_Xoves_Venres_Sábado".split("_"), - weekdaysShort : "Dom._Lun._Mar._Mér._Xov._Ven._Sáb.".split("_"), - weekdaysMin : "Do_Lu_Ma_Mé_Xo_Ve_Sá".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var gl = moment.defineLocale('gl', { + months : 'Xaneiro_Febreiro_Marzo_Abril_Maio_Xuño_Xullo_Agosto_Setembro_Outubro_Novembro_Decembro'.split('_'), + monthsShort : 'Xan._Feb._Mar._Abr._Mai._Xuñ._Xul._Ago._Set._Out._Nov._Dec.'.split('_'), + weekdays : 'Domingo_Luns_Martes_Mércores_Xoves_Venres_Sábado'.split('_'), + weekdaysShort : 'Dom._Lun._Mar._Mér._Xov._Ven._Sáb.'.split('_'), + weekdaysMin : 'Do_Lu_Ma_Mé_Xo_Ve_Sá'.split('_'), longDateFormat : { - LT : "H:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY H:mm', + LLLL : 'dddd D MMMM YYYY H:mm' }, calendar : { sameDay : function () { @@ -44,28 +44,32 @@ }, relativeTime : { future : function (str) { - if (str === "uns segundos") { - return "nuns segundos"; + if (str === 'uns segundos') { + return 'nuns segundos'; } - return "en " + str; + return 'en ' + str; }, - past : "hai %s", - s : "uns segundos", - m : "un minuto", - mm : "%d minutos", - h : "unha hora", - hh : "%d horas", - d : "un día", - dd : "%d días", - M : "un mes", - MM : "%d meses", - y : "un ano", - yy : "%d anos" + past : 'hai %s', + s : 'uns segundos', + m : 'un minuto', + mm : '%d minutos', + h : 'unha hora', + hh : '%d horas', + d : 'un día', + dd : '%d días', + M : 'un mes', + MM : '%d meses', + y : 'un ano', + yy : '%d anos' }, + ordinalParse : /\d{1,2}º/, ordinal : '%dº', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return gl; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/he.js b/lib/javascripts/moment_locale/he.js index 0af4e0988d..6a259be8b6 100644 --- a/lib/javascripts/moment_locale/he.js +++ b/lib/javascripts/moment_locale/he.js @@ -1,34 +1,34 @@ -// moment.js locale configuration -// locale : Hebrew (he) -// author : Tomer Cohen : https://github.com/tomer -// author : Moshe Simantov : https://github.com/DevelopmentIL -// author : Tal Ater : https://github.com/TalAter +//! moment.js locale configuration +//! locale : Hebrew (he) +//! author : Tomer Cohen : https://github.com/tomer +//! author : Moshe Simantov : https://github.com/DevelopmentIL +//! author : Tal Ater : https://github.com/TalAter -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('he', { - months : "ינואר_פברואר_מרץ_אפריל_מאי_יוני_יולי_אוגוסט_ספטמבר_אוקטובר_נובמבר_דצמבר".split("_"), - monthsShort : "ינו׳_פבר׳_מרץ_אפר׳_מאי_יוני_יולי_אוג׳_ספט׳_אוק׳_נוב׳_דצמ׳".split("_"), - weekdays : "ראשון_שני_שלישי_רביעי_חמישי_שישי_שבת".split("_"), - weekdaysShort : "א׳_ב׳_ג׳_ד׳_ה׳_ו׳_ש׳".split("_"), - weekdaysMin : "א_ב_ג_ד_ה_ו_ש".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var he = moment.defineLocale('he', { + months : 'ינואר_פברואר_מרץ_אפריל_מאי_יוני_יולי_אוגוסט_ספטמבר_אוקטובר_נובמבר_דצמבר'.split('_'), + monthsShort : 'ינו׳_פבר׳_מרץ_אפר׳_מאי_יוני_יולי_אוג׳_ספט׳_אוק׳_נוב׳_דצמ׳'.split('_'), + weekdays : 'ראשון_שני_שלישי_רביעי_חמישי_שישי_שבת'.split('_'), + weekdaysShort : 'א׳_ב׳_ג׳_ד׳_ה׳_ו׳_ש׳'.split('_'), + weekdaysMin : 'א_ב_ג_ד_ה_ו_ש'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D [ב]MMMM YYYY", - LLL : "D [ב]MMMM YYYY LT", - LLLL : "dddd, D [ב]MMMM YYYY LT", - l : "D/M/YYYY", - ll : "D MMM YYYY", - lll : "D MMM YYYY LT", - llll : "ddd, D MMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D [ב]MMMM YYYY', + LLL : 'D [ב]MMMM YYYY HH:mm', + LLLL : 'dddd, D [ב]MMMM YYYY HH:mm', + l : 'D/M/YYYY', + ll : 'D MMM YYYY', + lll : 'D MMM YYYY HH:mm', + llll : 'ddd, D MMM YYYY HH:mm' }, calendar : { sameDay : '[היום ב־]LT', @@ -39,39 +39,44 @@ sameElse : 'L' }, relativeTime : { - future : "בעוד %s", - past : "לפני %s", - s : "מספר שניות", - m : "דקה", - mm : "%d דקות", - h : "שעה", + future : 'בעוד %s', + past : 'לפני %s', + s : 'מספר שניות', + m : 'דקה', + mm : '%d דקות', + h : 'שעה', hh : function (number) { if (number === 2) { - return "שעתיים"; + return 'שעתיים'; } - return number + " שעות"; + return number + ' שעות'; }, - d : "יום", + d : 'יום', dd : function (number) { if (number === 2) { - return "יומיים"; + return 'יומיים'; } - return number + " ימים"; + return number + ' ימים'; }, - M : "חודש", + M : 'חודש', MM : function (number) { if (number === 2) { - return "חודשיים"; + return 'חודשיים'; } - return number + " חודשים"; + return number + ' חודשים'; }, - y : "שנה", + y : 'שנה', yy : function (number) { if (number === 2) { - return "שנתיים"; + return 'שנתיים'; + } else if (number % 10 === 0 && number !== 10) { + return number + ' שנה'; } - return number + " שנים"; + return number + ' שנים'; } } }); -})); + + return he; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/hi.js b/lib/javascripts/moment_locale/hi.js index 6dd7098c27..0542ef791e 100644 --- a/lib/javascripts/moment_locale/hi.js +++ b/lib/javascripts/moment_locale/hi.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : hindi (hi) -// author : Mayank Singhal : https://github.com/mayanksinghal +//! moment.js locale configuration +//! locale : hindi (hi) +//! author : Mayank Singhal : https://github.com/mayanksinghal + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var symbolMap = { '1': '१', '2': '२', @@ -36,18 +35,19 @@ '०': '0' }; - return moment.defineLocale('hi', { - months : 'जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर'.split("_"), - monthsShort : 'जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.'.split("_"), - weekdays : 'रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार'.split("_"), - weekdaysShort : 'रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि'.split("_"), - weekdaysMin : 'र_सो_मं_बु_गु_शु_श'.split("_"), + var hi = moment.defineLocale('hi', { + months : 'जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर'.split('_'), + monthsShort : 'जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.'.split('_'), + weekdays : 'रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार'.split('_'), + weekdaysShort : 'रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि'.split('_'), + weekdaysMin : 'र_सो_मं_बु_गु_शु_श'.split('_'), longDateFormat : { - LT : "A h:mm बजे", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY, LT", - LLLL : "dddd, D MMMM YYYY, LT" + LT : 'A h:mm बजे', + LTS : 'A h:mm:ss बजे', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY, A h:mm बजे', + LLLL : 'dddd, D MMMM YYYY, A h:mm बजे' }, calendar : { sameDay : '[आज] LT', @@ -58,19 +58,19 @@ sameElse : 'L' }, relativeTime : { - future : "%s में", - past : "%s पहले", - s : "कुछ ही क्षण", - m : "एक मिनट", - mm : "%d मिनट", - h : "एक घंटा", - hh : "%d घंटे", - d : "एक दिन", - dd : "%d दिन", - M : "एक महीने", - MM : "%d महीने", - y : "एक वर्ष", - yy : "%d वर्ष" + future : '%s में', + past : '%s पहले', + s : 'कुछ ही क्षण', + m : 'एक मिनट', + mm : '%d मिनट', + h : 'एक घंटा', + hh : '%d घंटे', + d : 'एक दिन', + dd : '%d दिन', + M : 'एक महीने', + MM : '%d महीने', + y : 'एक वर्ष', + yy : '%d वर्ष' }, preparse: function (string) { return string.replace(/[१२३४५६७८९०]/g, function (match) { @@ -84,17 +84,32 @@ }, // Hindi notation for meridiems are quite fuzzy in practice. While there exists // a rigid notion of a 'Pahar' it is not used as rigidly in modern Hindi. + meridiemParse: /रात|सुबह|दोपहर|शाम/, + meridiemHour : function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === 'रात') { + return hour < 4 ? hour : hour + 12; + } else if (meridiem === 'सुबह') { + return hour; + } else if (meridiem === 'दोपहर') { + return hour >= 10 ? hour : hour + 12; + } else if (meridiem === 'शाम') { + return hour + 12; + } + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { - return "रात"; + return 'रात'; } else if (hour < 10) { - return "सुबह"; + return 'सुबह'; } else if (hour < 17) { - return "दोपहर"; + return 'दोपहर'; } else if (hour < 20) { - return "शाम"; + return 'शाम'; } else { - return "रात"; + return 'रात'; } }, week : { @@ -102,4 +117,7 @@ doy : 6 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return hi; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/hr.js b/lib/javascripts/moment_locale/hr.js index 20fe8c1d66..26923838c9 100644 --- a/lib/javascripts/moment_locale/hr.js +++ b/lib/javascripts/moment_locale/hr.js @@ -1,20 +1,17 @@ -// moment.js locale configuration -// locale : hrvatski (hr) -// author : Bojan Marković : https://github.com/bmarkovic +//! moment.js locale configuration +//! locale : hrvatski (hr) +//! author : Bojan Marković : https://github.com/bmarkovic + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; -// based on (sl) translation by Robert Sedovšek -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function translate(number, withoutSuffix, key) { - var result = number + " "; + var result = number + ' '; switch (key) { case 'm': return withoutSuffix ? 'jedna minuta' : 'jedne minute'; @@ -66,23 +63,26 @@ } } - return moment.defineLocale('hr', { - months : "sječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_"), - monthsShort : "sje._vel._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"), - weekdays : "nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota".split("_"), - weekdaysShort : "ned._pon._uto._sri._čet._pet._sub.".split("_"), - weekdaysMin : "ne_po_ut_sr_če_pe_su".split("_"), + var hr = moment.defineLocale('hr', { + months : { + format: 'siječnja_veljače_ožujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca'.split('_'), + standalone: 'siječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac'.split('_') + }, + monthsShort : 'sij._velj._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.'.split('_'), + weekdays : 'nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota'.split('_'), + weekdaysShort : 'ned._pon._uto._sri._čet._pet._sub.'.split('_'), + weekdaysMin : 'ne_po_ut_sr_če_pe_su'.split('_'), longDateFormat : { - LT : "H:mm", - L : "DD. MM. YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY LT", - LLLL : "dddd, D. MMMM YYYY LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'DD. MM. YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY H:mm', + LLLL : 'dddd, D. MMMM YYYY H:mm' }, calendar : { sameDay : '[danas u] LT', nextDay : '[sutra u] LT', - nextWeek : function () { switch (this.day()) { case 0: @@ -116,24 +116,28 @@ sameElse : 'L' }, relativeTime : { - future : "za %s", - past : "prije %s", - s : "par sekundi", + future : 'za %s', + past : 'prije %s', + s : 'par sekundi', m : translate, mm : translate, h : translate, hh : translate, - d : "dan", + d : 'dan', dd : translate, - M : "mjesec", + M : 'mjesec', MM : translate, - y : "godinu", + y : 'godinu', yy : translate }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return hr; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/hu.js b/lib/javascripts/moment_locale/hu.js index 910f0868b5..2708672742 100644 --- a/lib/javascripts/moment_locale/hu.js +++ b/lib/javascripts/moment_locale/hu.js @@ -1,22 +1,19 @@ -// moment.js locale configuration -// locale : hungarian (hu) -// author : Adam Brunner : https://github.com/adambrunner +//! moment.js locale configuration +//! locale : hungarian (hu) +//! author : Adam Brunner : https://github.com/adambrunner + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var weekEndings = 'vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton'.split(' '); - function translate(number, withoutSuffix, key, isFuture) { var num = number, suffix; - switch (key) { case 's': return (isFuture || withoutSuffix) ? 'néhány másodperc' : 'néhány másodperce'; @@ -41,26 +38,29 @@ case 'yy': return num + (isFuture || withoutSuffix ? ' év' : ' éve'); } - return ''; } - function week(isFuture) { return (isFuture ? '' : '[múlt] ') + '[' + weekEndings[this.day()] + '] LT[-kor]'; } - return moment.defineLocale('hu', { - months : "január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"), - monthsShort : "jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"), - weekdays : "vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"), - weekdaysShort : "vas_hét_kedd_sze_csüt_pén_szo".split("_"), - weekdaysMin : "v_h_k_sze_cs_p_szo".split("_"), + var hu = moment.defineLocale('hu', { + months : 'január_február_március_április_május_június_július_augusztus_szeptember_október_november_december'.split('_'), + monthsShort : 'jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec'.split('_'), + weekdays : 'vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat'.split('_'), + weekdaysShort : 'vas_hét_kedd_sze_csüt_pén_szo'.split('_'), + weekdaysMin : 'v_h_k_sze_cs_p_szo'.split('_'), longDateFormat : { - LT : "H:mm", - L : "YYYY.MM.DD.", - LL : "YYYY. MMMM D.", - LLL : "YYYY. MMMM D., LT", - LLLL : "YYYY. MMMM D., dddd LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'YYYY.MM.DD.', + LL : 'YYYY. MMMM D.', + LLL : 'YYYY. MMMM D. H:mm', + LLLL : 'YYYY. MMMM D., dddd H:mm' + }, + meridiemParse: /de|du/i, + isPM: function (input) { + return input.charAt(1).toLowerCase() === 'u'; }, meridiem : function (hours, minutes, isLower) { if (hours < 12) { @@ -82,8 +82,8 @@ sameElse : 'L' }, relativeTime : { - future : "%s múlva", - past : "%s", + future : '%s múlva', + past : '%s', s : translate, m : translate, mm : translate, @@ -96,10 +96,14 @@ y : translate, yy : translate }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return hu; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/hy-am.js b/lib/javascripts/moment_locale/hy-am.js index b6984a2794..7350bfbe45 100644 --- a/lib/javascripts/moment_locale/hy-am.js +++ b/lib/javascripts/moment_locale/hy-am.js @@ -1,53 +1,31 @@ -// moment.js locale configuration -// locale : Armenian (hy-am) -// author : Armendarabyan : https://github.com/armendarabyan +//! moment.js locale configuration +//! locale : Armenian (hy-am) +//! author : Armendarabyan : https://github.com/armendarabyan -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - function monthsCaseReplace(m, format) { - var months = { - 'nominative': 'հունվար_փետրվար_մարտ_ապրիլ_մայիս_հունիս_հուլիս_օգոստոս_սեպտեմբեր_հոկտեմբեր_նոյեմբեր_դեկտեմբեր'.split('_'), - 'accusative': 'հունվարի_փետրվարի_մարտի_ապրիլի_մայիսի_հունիսի_հուլիսի_օգոստոսի_սեպտեմբերի_հոկտեմբերի_նոյեմբերի_դեկտեմբերի'.split('_') +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var hy_am = moment.defineLocale('hy-am', { + months : { + format: 'հունվարի_փետրվարի_մարտի_ապրիլի_մայիսի_հունիսի_հուլիսի_օգոստոսի_սեպտեմբերի_հոկտեմբերի_նոյեմբերի_դեկտեմբերի'.split('_'), + standalone: 'հունվար_փետրվար_մարտ_ապրիլ_մայիս_հունիս_հուլիս_օգոստոս_սեպտեմբեր_հոկտեմբեր_նոյեմբեր_դեկտեմբեր'.split('_') }, - - nounCase = (/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/).test(format) ? - 'accusative' : - 'nominative'; - - return months[nounCase][m.month()]; - } - - function monthsShortCaseReplace(m, format) { - var monthsShort = 'հնվ_փտր_մրտ_ապր_մյս_հնս_հլս_օգս_սպտ_հկտ_նմբ_դկտ'.split('_'); - - return monthsShort[m.month()]; - } - - function weekdaysCaseReplace(m, format) { - var weekdays = 'կիրակի_երկուշաբթի_երեքշաբթի_չորեքշաբթի_հինգշաբթի_ուրբաթ_շաբաթ'.split('_'); - - return weekdays[m.day()]; - } - - return moment.defineLocale('hy-am', { - months : monthsCaseReplace, - monthsShort : monthsShortCaseReplace, - weekdays : weekdaysCaseReplace, - weekdaysShort : "կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ".split("_"), - weekdaysMin : "կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ".split("_"), + monthsShort : 'հնվ_փտր_մրտ_ապր_մյս_հնս_հլս_օգս_սպտ_հկտ_նմբ_դկտ'.split('_'), + weekdays : 'կիրակի_երկուշաբթի_երեքշաբթի_չորեքշաբթի_հինգշաբթի_ուրբաթ_շաբաթ'.split('_'), + weekdaysShort : 'կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ'.split('_'), + weekdaysMin : 'կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD.MM.YYYY", - LL : "D MMMM YYYY թ.", - LLL : "D MMMM YYYY թ., LT", - LLLL : "dddd, D MMMM YYYY թ., LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY թ.', + LLL : 'D MMMM YYYY թ., HH:mm', + LLLL : 'dddd, D MMMM YYYY թ., HH:mm' }, calendar : { sameDay: '[այսօր] LT', @@ -62,33 +40,36 @@ sameElse: 'L' }, relativeTime : { - future : "%s հետո", - past : "%s առաջ", - s : "մի քանի վայրկյան", - m : "րոպե", - mm : "%d րոպե", - h : "ժամ", - hh : "%d ժամ", - d : "օր", - dd : "%d օր", - M : "ամիս", - MM : "%d ամիս", - y : "տարի", - yy : "%d տարի" + future : '%s հետո', + past : '%s առաջ', + s : 'մի քանի վայրկյան', + m : 'րոպե', + mm : '%d րոպե', + h : 'ժամ', + hh : '%d ժամ', + d : 'օր', + dd : '%d օր', + M : 'ամիս', + MM : '%d ամիս', + y : 'տարի', + yy : '%d տարի' + }, + meridiemParse: /գիշերվա|առավոտվա|ցերեկվա|երեկոյան/, + isPM: function (input) { + return /^(ցերեկվա|երեկոյան)$/.test(input); }, - meridiem : function (hour) { if (hour < 4) { - return "գիշերվա"; + return 'գիշերվա'; } else if (hour < 12) { - return "առավոտվա"; + return 'առավոտվա'; } else if (hour < 17) { - return "ցերեկվա"; + return 'ցերեկվա'; } else { - return "երեկոյան"; + return 'երեկոյան'; } }, - + ordinalParse: /\d{1,2}|\d{1,2}-(ին|րդ)/, ordinal: function (number, period) { switch (period) { case 'DDD': @@ -103,10 +84,12 @@ return number; } }, - week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return hy_am; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/id.js b/lib/javascripts/moment_locale/id.js index 6043f30a23..09461a5d5a 100644 --- a/lib/javascripts/moment_locale/id.js +++ b/lib/javascripts/moment_locale/id.js @@ -1,29 +1,42 @@ -// moment.js locale configuration -// locale : Bahasa Indonesia (id) -// author : Mohammad Satrio Utomo : https://github.com/tyok -// reference: http://id.wikisource.org/wiki/Pedoman_Umum_Ejaan_Bahasa_Indonesia_yang_Disempurnakan +//! moment.js locale configuration +//! locale : Bahasa Indonesia (id) +//! author : Mohammad Satrio Utomo : https://github.com/tyok +//! reference: http://id.wikisource.org/wiki/Pedoman_Umum_Ejaan_Bahasa_Indonesia_yang_Disempurnakan -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('id', { - months : "Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"), - monthsShort : "Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"), - weekdays : "Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"), - weekdaysShort : "Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"), - weekdaysMin : "Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var id = moment.defineLocale('id', { + months : 'Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember'.split('_'), + monthsShort : 'Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des'.split('_'), + weekdays : 'Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu'.split('_'), + weekdaysShort : 'Min_Sen_Sel_Rab_Kam_Jum_Sab'.split('_'), + weekdaysMin : 'Mg_Sn_Sl_Rb_Km_Jm_Sb'.split('_'), longDateFormat : { - LT : "HH.mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY [pukul] LT", - LLLL : "dddd, D MMMM YYYY [pukul] LT" + LT : 'HH.mm', + LTS : 'HH.mm.ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY [pukul] HH.mm', + LLLL : 'dddd, D MMMM YYYY [pukul] HH.mm' + }, + meridiemParse: /pagi|siang|sore|malam/, + meridiemHour : function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === 'pagi') { + return hour; + } else if (meridiem === 'siang') { + return hour >= 11 ? hour : hour + 12; + } else if (meridiem === 'sore' || meridiem === 'malam') { + return hour + 12; + } }, meridiem : function (hours, minutes, isLower) { if (hours < 11) { @@ -45,23 +58,26 @@ sameElse : 'L' }, relativeTime : { - future : "dalam %s", - past : "%s yang lalu", - s : "beberapa detik", - m : "semenit", - mm : "%d menit", - h : "sejam", - hh : "%d jam", - d : "sehari", - dd : "%d hari", - M : "sebulan", - MM : "%d bulan", - y : "setahun", - yy : "%d tahun" + future : 'dalam %s', + past : '%s yang lalu', + s : 'beberapa detik', + m : 'semenit', + mm : '%d menit', + h : 'sejam', + hh : '%d jam', + d : 'sehari', + dd : '%d hari', + M : 'sebulan', + MM : '%d bulan', + y : 'setahun', + yy : '%d tahun' }, week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return id; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/is.js b/lib/javascripts/moment_locale/is.js index ed2240644f..f1ed257a47 100644 --- a/lib/javascripts/moment_locale/is.js +++ b/lib/javascripts/moment_locale/is.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : icelandic (is) -// author : Hinrik Örn Sigurðsson : https://github.com/hinrik +//! moment.js locale configuration +//! locale : icelandic (is) +//! author : Hinrik Örn Sigurðsson : https://github.com/hinrik + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function plural(n) { if (n % 100 === 11) { return true; @@ -19,9 +18,8 @@ } return true; } - function translate(number, withoutSuffix, key, isFuture) { - var result = number + " "; + var result = number + ' '; switch (key) { case 's': return withoutSuffix || isFuture ? 'nokkrar sekúndur' : 'nokkrum sekúndum'; @@ -79,18 +77,19 @@ } } - return moment.defineLocale('is', { - months : "janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"), - monthsShort : "jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"), - weekdays : "sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"), - weekdaysShort : "sun_mán_þri_mið_fim_fös_lau".split("_"), - weekdaysMin : "Su_Má_Þr_Mi_Fi_Fö_La".split("_"), + var is = moment.defineLocale('is', { + months : 'janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember'.split('_'), + monthsShort : 'jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des'.split('_'), + weekdays : 'sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur'.split('_'), + weekdaysShort : 'sun_mán_þri_mið_fim_fös_lau'.split('_'), + weekdaysMin : 'Su_Má_Þr_Mi_Fi_Fö_La'.split('_'), longDateFormat : { - LT : "H:mm", - L : "DD/MM/YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY [kl.] LT", - LLLL : "dddd, D. MMMM YYYY [kl.] LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY [kl.] H:mm', + LLLL : 'dddd, D. MMMM YYYY [kl.] H:mm' }, calendar : { sameDay : '[í dag kl.] LT', @@ -101,12 +100,12 @@ sameElse : 'L' }, relativeTime : { - future : "eftir %s", - past : "fyrir %s síðan", + future : 'eftir %s', + past : 'fyrir %s síðan', s : translate, m : translate, mm : translate, - h : "klukkustund", + h : 'klukkustund', hh : translate, d : translate, dd : translate, @@ -115,10 +114,14 @@ y : translate, yy : translate }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return is; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/it.js b/lib/javascripts/moment_locale/it.js index a151ccc62a..e8b2c950c7 100644 --- a/lib/javascripts/moment_locale/it.js +++ b/lib/javascripts/moment_locale/it.js @@ -1,59 +1,70 @@ -// moment.js locale configuration -// locale : italian (it) -// author : Lorenzo : https://github.com/aliem -// author: Mattia Larentis: https://github.com/nostalgiaz +//! moment.js locale configuration +//! locale : italian (it) +//! author : Lorenzo : https://github.com/aliem +//! author: Mattia Larentis: https://github.com/nostalgiaz -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('it', { - months : "gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"), - monthsShort : "gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"), - weekdays : "Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"), - weekdaysShort : "Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"), - weekdaysMin : "D_L_Ma_Me_G_V_S".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var it = moment.defineLocale('it', { + months : 'gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre'.split('_'), + monthsShort : 'gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic'.split('_'), + weekdays : 'Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato'.split('_'), + weekdaysShort : 'Dom_Lun_Mar_Mer_Gio_Ven_Sab'.split('_'), + weekdaysMin : 'Do_Lu_Ma_Me_Gi_Ve_Sa'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' }, calendar : { sameDay: '[Oggi alle] LT', nextDay: '[Domani alle] LT', nextWeek: 'dddd [alle] LT', lastDay: '[Ieri alle] LT', - lastWeek: '[lo scorso] dddd [alle] LT', + lastWeek: function () { + switch (this.day()) { + case 0: + return '[la scorsa] dddd [alle] LT'; + default: + return '[lo scorso] dddd [alle] LT'; + } + }, sameElse: 'L' }, relativeTime : { future : function (s) { - return ((/^[0-9].+$/).test(s) ? "tra" : "in") + " " + s; + return ((/^[0-9].+$/).test(s) ? 'tra' : 'in') + ' ' + s; }, - past : "%s fa", - s : "alcuni secondi", - m : "un minuto", - mm : "%d minuti", - h : "un'ora", - hh : "%d ore", - d : "un giorno", - dd : "%d giorni", - M : "un mese", - MM : "%d mesi", - y : "un anno", - yy : "%d anni" + past : '%s fa', + s : 'alcuni secondi', + m : 'un minuto', + mm : '%d minuti', + h : 'un\'ora', + hh : '%d ore', + d : 'un giorno', + dd : '%d giorni', + M : 'un mese', + MM : '%d mesi', + y : 'un anno', + yy : '%d anni' }, + ordinalParse : /\d{1,2}º/, ordinal: '%dº', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return it; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ja.js b/lib/javascripts/moment_locale/ja.js index 34c4b890d8..6c6e85930c 100644 --- a/lib/javascripts/moment_locale/ja.js +++ b/lib/javascripts/moment_locale/ja.js @@ -1,34 +1,38 @@ -// moment.js locale configuration -// locale : japanese (ja) -// author : LI Long : https://github.com/baryon +//! moment.js locale configuration +//! locale : japanese (ja) +//! author : LI Long : https://github.com/baryon -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('ja', { - months : "1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"), - monthsShort : "1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"), - weekdays : "日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"), - weekdaysShort : "日_月_火_水_木_金_土".split("_"), - weekdaysMin : "日_月_火_水_木_金_土".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var ja = moment.defineLocale('ja', { + months : '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'), + monthsShort : '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'), + weekdays : '日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日'.split('_'), + weekdaysShort : '日_月_火_水_木_金_土'.split('_'), + weekdaysMin : '日_月_火_水_木_金_土'.split('_'), longDateFormat : { - LT : "Ah時m分", - L : "YYYY/MM/DD", - LL : "YYYY年M月D日", - LLL : "YYYY年M月D日LT", - LLLL : "YYYY年M月D日LT dddd" + LT : 'Ah時m分', + LTS : 'Ah時m分s秒', + L : 'YYYY/MM/DD', + LL : 'YYYY年M月D日', + LLL : 'YYYY年M月D日Ah時m分', + LLLL : 'YYYY年M月D日Ah時m分 dddd' + }, + meridiemParse: /午前|午後/i, + isPM : function (input) { + return input === '午後'; }, meridiem : function (hour, minute, isLower) { if (hour < 12) { - return "午前"; + return '午前'; } else { - return "午後"; + return '午後'; } }, calendar : { @@ -40,19 +44,22 @@ sameElse : 'L' }, relativeTime : { - future : "%s後", - past : "%s前", - s : "数秒", - m : "1分", - mm : "%d分", - h : "1時間", - hh : "%d時間", - d : "1日", - dd : "%d日", - M : "1ヶ月", - MM : "%dヶ月", - y : "1年", - yy : "%d年" + future : '%s後', + past : '%s前', + s : '数秒', + m : '1分', + mm : '%d分', + h : '1時間', + hh : '%d時間', + d : '1日', + dd : '%d日', + M : '1ヶ月', + MM : '%dヶ月', + y : '1年', + yy : '%d年' } }); -})); + + return ja; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/jv.js b/lib/javascripts/moment_locale/jv.js new file mode 100644 index 0000000000..d3b85a4c06 --- /dev/null +++ b/lib/javascripts/moment_locale/jv.js @@ -0,0 +1,83 @@ +//! moment.js locale configuration +//! locale : Boso Jowo (jv) +//! author : Rony Lantip : https://github.com/lantip +//! reference: http://jv.wikipedia.org/wiki/Basa_Jawa + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var jv = moment.defineLocale('jv', { + months : 'Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_Nopember_Desember'.split('_'), + monthsShort : 'Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nop_Des'.split('_'), + weekdays : 'Minggu_Senen_Seloso_Rebu_Kemis_Jemuwah_Septu'.split('_'), + weekdaysShort : 'Min_Sen_Sel_Reb_Kem_Jem_Sep'.split('_'), + weekdaysMin : 'Mg_Sn_Sl_Rb_Km_Jm_Sp'.split('_'), + longDateFormat : { + LT : 'HH.mm', + LTS : 'HH.mm.ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY [pukul] HH.mm', + LLLL : 'dddd, D MMMM YYYY [pukul] HH.mm' + }, + meridiemParse: /enjing|siyang|sonten|ndalu/, + meridiemHour : function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === 'enjing') { + return hour; + } else if (meridiem === 'siyang') { + return hour >= 11 ? hour : hour + 12; + } else if (meridiem === 'sonten' || meridiem === 'ndalu') { + return hour + 12; + } + }, + meridiem : function (hours, minutes, isLower) { + if (hours < 11) { + return 'enjing'; + } else if (hours < 15) { + return 'siyang'; + } else if (hours < 19) { + return 'sonten'; + } else { + return 'ndalu'; + } + }, + calendar : { + sameDay : '[Dinten puniko pukul] LT', + nextDay : '[Mbenjang pukul] LT', + nextWeek : 'dddd [pukul] LT', + lastDay : '[Kala wingi pukul] LT', + lastWeek : 'dddd [kepengker pukul] LT', + sameElse : 'L' + }, + relativeTime : { + future : 'wonten ing %s', + past : '%s ingkang kepengker', + s : 'sawetawis detik', + m : 'setunggal menit', + mm : '%d menit', + h : 'setunggal jam', + hh : '%d jam', + d : 'sedinten', + dd : '%d dinten', + M : 'sewulan', + MM : '%d wulan', + y : 'setaun', + yy : '%d taun' + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 7 // The week that contains Jan 1st is the first week of the year. + } + }); + + return jv; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ka.js b/lib/javascripts/moment_locale/ka.js index 3134524364..f052c3ac33 100644 --- a/lib/javascripts/moment_locale/ka.js +++ b/lib/javascripts/moment_locale/ka.js @@ -1,54 +1,35 @@ -// moment.js locale configuration -// locale : Georgian (ka) -// author : Irakli Janiashvili : https://github.com/irakli-janiashvili +//! moment.js locale configuration +//! locale : Georgian (ka) +//! author : Irakli Janiashvili : https://github.com/irakli-janiashvili -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - function monthsCaseReplace(m, format) { - var months = { - 'nominative': 'იანვარი_თებერვალი_მარტი_აპრილი_მაისი_ივნისი_ივლისი_აგვისტო_სექტემბერი_ოქტომბერი_ნოემბერი_დეკემბერი'.split('_'), - 'accusative': 'იანვარს_თებერვალს_მარტს_აპრილის_მაისს_ივნისს_ივლისს_აგვისტს_სექტემბერს_ოქტომბერს_ნოემბერს_დეკემბერს'.split('_') +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var ka = moment.defineLocale('ka', { + months : { + standalone: 'იანვარი_თებერვალი_მარტი_აპრილი_მაისი_ივნისი_ივლისი_აგვისტო_სექტემბერი_ოქტომბერი_ნოემბერი_დეკემბერი'.split('_'), + format: 'იანვარს_თებერვალს_მარტს_აპრილის_მაისს_ივნისს_ივლისს_აგვისტს_სექტემბერს_ოქტომბერს_ნოემბერს_დეკემბერს'.split('_') }, - - nounCase = (/D[oD] *MMMM?/).test(format) ? - 'accusative' : - 'nominative'; - - return months[nounCase][m.month()]; - } - - function weekdaysCaseReplace(m, format) { - var weekdays = { - 'nominative': 'კვირა_ორშაბათი_სამშაბათი_ოთხშაბათი_ხუთშაბათი_პარასკევი_შაბათი'.split('_'), - 'accusative': 'კვირას_ორშაბათს_სამშაბათს_ოთხშაბათს_ხუთშაბათს_პარასკევს_შაბათს'.split('_') + monthsShort : 'იან_თებ_მარ_აპრ_მაი_ივნ_ივლ_აგვ_სექ_ოქტ_ნოე_დეკ'.split('_'), + weekdays : { + standalone: 'კვირა_ორშაბათი_სამშაბათი_ოთხშაბათი_ხუთშაბათი_პარასკევი_შაბათი'.split('_'), + format: 'კვირას_ორშაბათს_სამშაბათს_ოთხშაბათს_ხუთშაბათს_პარასკევს_შაბათს'.split('_'), + isFormat: /(წინა|შემდეგ)/ }, - - nounCase = (/(წინა|შემდეგ)/).test(format) ? - 'accusative' : - 'nominative'; - - return weekdays[nounCase][m.day()]; - } - - return moment.defineLocale('ka', { - months : monthsCaseReplace, - monthsShort : "იან_თებ_მარ_აპრ_მაი_ივნ_ივლ_აგვ_სექ_ოქტ_ნოე_დეკ".split("_"), - weekdays : weekdaysCaseReplace, - weekdaysShort : "კვი_ორშ_სამ_ოთხ_ხუთ_პარ_შაბ".split("_"), - weekdaysMin : "კვ_ორ_სა_ოთ_ხუ_პა_შა".split("_"), + weekdaysShort : 'კვი_ორშ_სამ_ოთხ_ხუთ_პარ_შაბ'.split('_'), + weekdaysMin : 'კვ_ორ_სა_ოთ_ხუ_პა_შა'.split('_'), longDateFormat : { - LT : "h:mm A", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'h:mm A', + LTS : 'h:mm:ss A', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY h:mm A', + LLLL : 'dddd, D MMMM YYYY h:mm A' }, calendar : { sameDay : '[დღეს] LT[-ზე]', @@ -61,47 +42,48 @@ relativeTime : { future : function (s) { return (/(წამი|წუთი|საათი|წელი)/).test(s) ? - s.replace(/ი$/, "ში") : - s + "ში"; + s.replace(/ი$/, 'ში') : + s + 'ში'; }, past : function (s) { if ((/(წამი|წუთი|საათი|დღე|თვე)/).test(s)) { - return s.replace(/(ი|ე)$/, "ის წინ"); + return s.replace(/(ი|ე)$/, 'ის წინ'); } if ((/წელი/).test(s)) { - return s.replace(/წელი$/, "წლის წინ"); + return s.replace(/წელი$/, 'წლის წინ'); } }, - s : "რამდენიმე წამი", - m : "წუთი", - mm : "%d წუთი", - h : "საათი", - hh : "%d საათი", - d : "დღე", - dd : "%d დღე", - M : "თვე", - MM : "%d თვე", - y : "წელი", - yy : "%d წელი" + s : 'რამდენიმე წამი', + m : 'წუთი', + mm : '%d წუთი', + h : 'საათი', + hh : '%d საათი', + d : 'დღე', + dd : '%d დღე', + M : 'თვე', + MM : '%d თვე', + y : 'წელი', + yy : '%d წელი' }, + ordinalParse: /0|1-ლი|მე-\d{1,2}|\d{1,2}-ე/, ordinal : function (number) { if (number === 0) { return number; } - if (number === 1) { - return number + "-ლი"; + return number + '-ლი'; } - if ((number < 20) || (number <= 100 && (number % 20 === 0)) || (number % 100 === 0)) { - return "მე-" + number; + return 'მე-' + number; } - - return number + "-ე"; + return number + '-ე'; }, week : { dow : 1, doy : 7 } }); -})); + + return ka; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/kk.js b/lib/javascripts/moment_locale/kk.js new file mode 100644 index 0000000000..81acd0bf62 --- /dev/null +++ b/lib/javascripts/moment_locale/kk.js @@ -0,0 +1,87 @@ +//! moment.js locale configuration +//! locale : kazakh (kk) +//! authors : Nurlan Rakhimzhanov : https://github.com/nurlan + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var suffixes = { + 0: '-ші', + 1: '-ші', + 2: '-ші', + 3: '-ші', + 4: '-ші', + 5: '-ші', + 6: '-шы', + 7: '-ші', + 8: '-ші', + 9: '-шы', + 10: '-шы', + 20: '-шы', + 30: '-шы', + 40: '-шы', + 50: '-ші', + 60: '-шы', + 70: '-ші', + 80: '-ші', + 90: '-шы', + 100: '-ші' + }; + + var kk = moment.defineLocale('kk', { + months : 'Қаңтар_Ақпан_Наурыз_Сәуір_Мамыр_Маусым_Шілде_Тамыз_Қыркүйек_Қазан_Қараша_Желтоқсан'.split('_'), + monthsShort : 'Қаң_Ақп_Нау_Сәу_Мам_Мау_Шіл_Там_Қыр_Қаз_Қар_Жел'.split('_'), + weekdays : 'Жексенбі_Дүйсенбі_Сейсенбі_Сәрсенбі_Бейсенбі_Жұма_Сенбі'.split('_'), + weekdaysShort : 'Жек_Дүй_Сей_Сәр_Бей_Жұм_Сен'.split('_'), + weekdaysMin : 'Жк_Дй_Сй_Ср_Бй_Жм_Сн'.split('_'), + longDateFormat : { + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' + }, + calendar : { + sameDay : '[Бүгін сағат] LT', + nextDay : '[Ертең сағат] LT', + nextWeek : 'dddd [сағат] LT', + lastDay : '[Кеше сағат] LT', + lastWeek : '[Өткен аптаның] dddd [сағат] LT', + sameElse : 'L' + }, + relativeTime : { + future : '%s ішінде', + past : '%s бұрын', + s : 'бірнеше секунд', + m : 'бір минут', + mm : '%d минут', + h : 'бір сағат', + hh : '%d сағат', + d : 'бір күн', + dd : '%d күн', + M : 'бір ай', + MM : '%d ай', + y : 'бір жыл', + yy : '%d жыл' + }, + ordinalParse: /\d{1,2}-(ші|шы)/, + ordinal : function (number) { + var a = number % 10, + b = number >= 100 ? 100 : null; + return number + (suffixes[number] || suffixes[a] || suffixes[b]); + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 7 // The week that contains Jan 1st is the first week of the year. + } + }); + + return kk; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/km.js b/lib/javascripts/moment_locale/km.js index f457e8d130..56466c9138 100644 --- a/lib/javascripts/moment_locale/km.js +++ b/lib/javascripts/moment_locale/km.js @@ -1,31 +1,31 @@ -// moment.js locale configuration -// locale : khmer (km) -// author : Kruy Vanna : https://github.com/kruyvanna +//! moment.js locale configuration +//! locale : khmer (km) +//! author : Kruy Vanna : https://github.com/kruyvanna -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('km', { - months: "មករា_កុម្ភៈ_មិនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ".split("_"), - monthsShort: "មករា_កុម្ភៈ_មិនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ".split("_"), - weekdays: "អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍".split("_"), - weekdaysShort: "អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍".split("_"), - weekdaysMin: "អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var km = moment.defineLocale('km', { + months: 'មករា_កុម្ភៈ_មិនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ'.split('_'), + monthsShort: 'មករា_កុម្ភៈ_មិនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ'.split('_'), + weekdays: 'អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍'.split('_'), + weekdaysShort: 'អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍'.split('_'), + weekdaysMin: 'អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍'.split('_'), longDateFormat: { - LT: "HH:mm", - L: "DD/MM/YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY LT", - LLLL: "dddd, D MMMM YYYY LT" + LT: 'HH:mm', + LTS : 'HH:mm:ss', + L: 'DD/MM/YYYY', + LL: 'D MMMM YYYY', + LLL: 'D MMMM YYYY HH:mm', + LLLL: 'dddd, D MMMM YYYY HH:mm' }, calendar: { - sameDay: '[ថ្ងៃនៈ ម៉ោង] LT', + sameDay: '[ថ្ងៃនេះ ម៉ោង] LT', nextDay: '[ស្អែក ម៉ោង] LT', nextWeek: 'dddd [ម៉ោង] LT', lastDay: '[ម្សិលមិញ ម៉ោង] LT', @@ -33,23 +33,26 @@ sameElse: 'L' }, relativeTime: { - future: "%sទៀត", - past: "%sមុន", - s: "ប៉ុន្មានវិនាទី", - m: "មួយនាទី", - mm: "%d នាទី", - h: "មួយម៉ោង", - hh: "%d ម៉ោង", - d: "មួយថ្ងៃ", - dd: "%d ថ្ងៃ", - M: "មួយខែ", - MM: "%d ខែ", - y: "មួយឆ្នាំ", - yy: "%d ឆ្នាំ" + future: '%sទៀត', + past: '%sមុន', + s: 'ប៉ុន្មានវិនាទី', + m: 'មួយនាទី', + mm: '%d នាទី', + h: 'មួយម៉ោង', + hh: '%d ម៉ោង', + d: 'មួយថ្ងៃ', + dd: '%d ថ្ងៃ', + M: 'មួយខែ', + MM: '%d ខែ', + y: 'មួយឆ្នាំ', + yy: '%d ឆ្នាំ' }, week: { dow: 1, // Monday is the first day of the week. doy: 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return km; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ko.js b/lib/javascripts/moment_locale/ko.js index 7de2e51033..151554ab39 100644 --- a/lib/javascripts/moment_locale/ko.js +++ b/lib/javascripts/moment_locale/ko.js @@ -1,34 +1,32 @@ -// moment.js locale configuration -// locale : korean (ko) -// -// authors -// -// - Kyungwook, Park : https://github.com/kyungw00k -// - Jeeeyul Lee -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('ko', { - months : "1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"), - monthsShort : "1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"), - weekdays : "일요일_월요일_화요일_수요일_목요일_금요일_토요일".split("_"), - weekdaysShort : "일_월_화_수_목_금_토".split("_"), - weekdaysMin : "일_월_화_수_목_금_토".split("_"), +//! moment.js locale configuration +//! locale : korean (ko) +//! +//! authors +//! +//! - Kyungwook, Park : https://github.com/kyungw00k +//! - Jeeeyul Lee + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var ko = moment.defineLocale('ko', { + months : '1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월'.split('_'), + monthsShort : '1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월'.split('_'), + weekdays : '일요일_월요일_화요일_수요일_목요일_금요일_토요일'.split('_'), + weekdaysShort : '일_월_화_수_목_금_토'.split('_'), + weekdaysMin : '일_월_화_수_목_금_토'.split('_'), longDateFormat : { - LT : "A h시 mm분", - L : "YYYY.MM.DD", - LL : "YYYY년 MMMM D일", - LLL : "YYYY년 MMMM D일 LT", - LLLL : "YYYY년 MMMM D일 dddd LT" - }, - meridiem : function (hour, minute, isUpper) { - return hour < 12 ? '오전' : '오후'; + LT : 'A h시 m분', + LTS : 'A h시 m분 s초', + L : 'YYYY.MM.DD', + LL : 'YYYY년 MMMM D일', + LLL : 'YYYY년 MMMM D일 A h시 m분', + LLLL : 'YYYY년 MMMM D일 dddd A h시 m분' }, calendar : { sameDay : '오늘 LT', @@ -39,25 +37,32 @@ sameElse : 'L' }, relativeTime : { - future : "%s 후", - past : "%s 전", - s : "몇초", - ss : "%d초", - m : "일분", - mm : "%d분", - h : "한시간", - hh : "%d시간", - d : "하루", - dd : "%d일", - M : "한달", - MM : "%d달", - y : "일년", - yy : "%d년" + future : '%s 후', + past : '%s 전', + s : '몇초', + ss : '%d초', + m : '일분', + mm : '%d분', + h : '한시간', + hh : '%d시간', + d : '하루', + dd : '%d일', + M : '한달', + MM : '%d달', + y : '일년', + yy : '%d년' }, + ordinalParse : /\d{1,2}일/, ordinal : '%d일', - meridiemParse : /(오전|오후)/, + meridiemParse : /오전|오후/, isPM : function (token) { - return token === "오후"; + return token === '오후'; + }, + meridiem : function (hour, minute, isUpper) { + return hour < 12 ? '오전' : '오후'; } }); -})); + + return ko; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/lb.js b/lib/javascripts/moment_locale/lb.js index c878b79c13..6713c08a9c 100644 --- a/lib/javascripts/moment_locale/lb.js +++ b/lib/javascripts/moment_locale/lb.js @@ -1,20 +1,15 @@ -// moment.js locale configuration -// locale : Luxembourgish (lb) -// author : mweimerskirch : https://github.com/mweimerskirch, David Raison : https://github.com/kwisatz +//! moment.js locale configuration +//! locale : Luxembourgish (lb) +//! author : mweimerskirch : https://github.com/mweimerskirch, David Raison : https://github.com/kwisatz + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; -// Note: Luxembourgish has a very particular phonological rule ("Eifeler Regel") that causes the -// deletion of the final "n" in certain contexts. That's what the "eifelerRegelAppliesToWeekday" -// and "eifelerRegelAppliesToNumber" methods are meant for -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function processRelativeTime(number, withoutSuffix, key, isFuture) { var format = { 'm': ['eng Minutt', 'enger Minutt'], @@ -25,26 +20,23 @@ }; return withoutSuffix ? format[key][0] : format[key][1]; } - function processFutureTime(string) { var number = string.substr(0, string.indexOf(' ')); if (eifelerRegelAppliesToNumber(number)) { - return "a " + string; + return 'a ' + string; } - return "an " + string; + return 'an ' + string; } - function processPastTime(string) { var number = string.substr(0, string.indexOf(' ')); if (eifelerRegelAppliesToNumber(number)) { - return "viru " + string; + return 'viru ' + string; } - return "virun " + string; + return 'virun ' + string; } - /** - * Returns true if the word before the given number loses the "-n" ending. - * e.g. "an 10 Deeg" but "a 5 Deeg" + * Returns true if the word before the given number loses the '-n' ending. + * e.g. 'an 10 Deeg' but 'a 5 Deeg' * * @param number {integer} * @returns {boolean} @@ -83,27 +75,28 @@ } } - return moment.defineLocale('lb', { - months: "Januar_Februar_Mäerz_Abrëll_Mee_Juni_Juli_August_September_Oktober_November_Dezember".split("_"), - monthsShort: "Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"), - weekdays: "Sonndeg_Méindeg_Dënschdeg_Mëttwoch_Donneschdeg_Freideg_Samschdeg".split("_"), - weekdaysShort: "So._Mé._Dë._Më._Do._Fr._Sa.".split("_"), - weekdaysMin: "So_Mé_Dë_Më_Do_Fr_Sa".split("_"), + var lb = moment.defineLocale('lb', { + months: 'Januar_Februar_Mäerz_Abrëll_Mee_Juni_Juli_August_September_Oktober_November_Dezember'.split('_'), + monthsShort: 'Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.'.split('_'), + weekdays: 'Sonndeg_Méindeg_Dënschdeg_Mëttwoch_Donneschdeg_Freideg_Samschdeg'.split('_'), + weekdaysShort: 'So._Mé._Dë._Më._Do._Fr._Sa.'.split('_'), + weekdaysMin: 'So_Mé_Dë_Më_Do_Fr_Sa'.split('_'), longDateFormat: { - LT: "H:mm [Auer]", - L: "DD.MM.YYYY", - LL: "D. MMMM YYYY", - LLL: "D. MMMM YYYY LT", - LLLL: "dddd, D. MMMM YYYY LT" + LT: 'H:mm [Auer]', + LTS: 'H:mm:ss [Auer]', + L: 'DD.MM.YYYY', + LL: 'D. MMMM YYYY', + LLL: 'D. MMMM YYYY H:mm [Auer]', + LLLL: 'dddd, D. MMMM YYYY H:mm [Auer]' }, calendar: { - sameDay: "[Haut um] LT", - sameElse: "L", + sameDay: '[Haut um] LT', + sameElse: 'L', nextDay: '[Muer um] LT', nextWeek: 'dddd [um] LT', lastDay: '[Gëschter um] LT', lastWeek: function () { - // Different date string for "Dënschdeg" (Tuesday) and "Donneschdeg" (Thursday) due to phonological rule + // Different date string for 'Dënschdeg' (Tuesday) and 'Donneschdeg' (Thursday) due to phonological rule switch (this.day()) { case 2: case 4: @@ -116,22 +109,26 @@ relativeTime : { future : processFutureTime, past : processPastTime, - s : "e puer Sekonnen", + s : 'e puer Sekonnen', m : processRelativeTime, - mm : "%d Minutten", + mm : '%d Minutten', h : processRelativeTime, - hh : "%d Stonnen", + hh : '%d Stonnen', d : processRelativeTime, - dd : "%d Deeg", + dd : '%d Deeg', M : processRelativeTime, - MM : "%d Méint", + MM : '%d Méint', y : processRelativeTime, - yy : "%d Joer" + yy : '%d Joer' }, + ordinalParse: /\d{1,2}\./, ordinal: '%d.', week: { dow: 1, // Monday is the first day of the week. doy: 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return lb; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/lo.js b/lib/javascripts/moment_locale/lo.js new file mode 100644 index 0000000000..7475f27384 --- /dev/null +++ b/lib/javascripts/moment_locale/lo.js @@ -0,0 +1,69 @@ +//! moment.js locale configuration +//! locale : lao (lo) +//! author : Ryan Hart : https://github.com/ryanhart2 + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var lo = moment.defineLocale('lo', { + months : 'ມັງກອນ_ກຸມພາ_ມີນາ_ເມສາ_ພຶດສະພາ_ມິຖຸນາ_ກໍລະກົດ_ສິງຫາ_ກັນຍາ_ຕຸລາ_ພະຈິກ_ທັນວາ'.split('_'), + monthsShort : 'ມັງກອນ_ກຸມພາ_ມີນາ_ເມສາ_ພຶດສະພາ_ມິຖຸນາ_ກໍລະກົດ_ສິງຫາ_ກັນຍາ_ຕຸລາ_ພະຈິກ_ທັນວາ'.split('_'), + weekdays : 'ອາທິດ_ຈັນ_ອັງຄານ_ພຸດ_ພະຫັດ_ສຸກ_ເສົາ'.split('_'), + weekdaysShort : 'ທິດ_ຈັນ_ອັງຄານ_ພຸດ_ພະຫັດ_ສຸກ_ເສົາ'.split('_'), + weekdaysMin : 'ທ_ຈ_ອຄ_ພ_ພຫ_ສກ_ສ'.split('_'), + longDateFormat : { + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'ວັນdddd D MMMM YYYY HH:mm' + }, + meridiemParse: /ຕອນເຊົ້າ|ຕອນແລງ/, + isPM: function (input) { + return input === 'ຕອນແລງ'; + }, + meridiem : function (hour, minute, isLower) { + if (hour < 12) { + return 'ຕອນເຊົ້າ'; + } else { + return 'ຕອນແລງ'; + } + }, + calendar : { + sameDay : '[ມື້ນີ້ເວລາ] LT', + nextDay : '[ມື້ອື່ນເວລາ] LT', + nextWeek : '[ວັນ]dddd[ໜ້າເວລາ] LT', + lastDay : '[ມື້ວານນີ້ເວລາ] LT', + lastWeek : '[ວັນ]dddd[ແລ້ວນີ້ເວລາ] LT', + sameElse : 'L' + }, + relativeTime : { + future : 'ອີກ %s', + past : '%sຜ່ານມາ', + s : 'ບໍ່ເທົ່າໃດວິນາທີ', + m : '1 ນາທີ', + mm : '%d ນາທີ', + h : '1 ຊົ່ວໂມງ', + hh : '%d ຊົ່ວໂມງ', + d : '1 ມື້', + dd : '%d ມື້', + M : '1 ເດືອນ', + MM : '%d ເດືອນ', + y : '1 ປີ', + yy : '%d ປີ' + }, + ordinalParse: /(ທີ່)\d{1,2}/, + ordinal : function (number) { + return 'ທີ່' + number; + } + }); + + return lo; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/lt.js b/lib/javascripts/moment_locale/lt.js index 7d7b93f10a..72566bfee9 100644 --- a/lib/javascripts/moment_locale/lt.js +++ b/lib/javascripts/moment_locale/lt.js @@ -1,52 +1,45 @@ -// moment.js locale configuration -// locale : Lithuanian (lt) -// author : Mindaugas Mozūras : https://github.com/mmozuras +//! moment.js locale configuration +//! locale : Lithuanian (lt) +//! author : Mindaugas Mozūras : https://github.com/mmozuras + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var units = { - "m" : "minutė_minutės_minutę", - "mm": "minutės_minučių_minutes", - "h" : "valanda_valandos_valandą", - "hh": "valandos_valandų_valandas", - "d" : "diena_dienos_dieną", - "dd": "dienos_dienų_dienas", - "M" : "mėnuo_mėnesio_mėnesį", - "MM": "mėnesiai_mėnesių_mėnesius", - "y" : "metai_metų_metus", - "yy": "metai_metų_metus" - }, - weekDays = "sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis".split("_"); - + 'm' : 'minutė_minutės_minutę', + 'mm': 'minutės_minučių_minutes', + 'h' : 'valanda_valandos_valandą', + 'hh': 'valandos_valandų_valandas', + 'd' : 'diena_dienos_dieną', + 'dd': 'dienos_dienų_dienas', + 'M' : 'mėnuo_mėnesio_mėnesį', + 'MM': 'mėnesiai_mėnesių_mėnesius', + 'y' : 'metai_metų_metus', + 'yy': 'metai_metų_metus' + }; function translateSeconds(number, withoutSuffix, key, isFuture) { if (withoutSuffix) { - return "kelios sekundės"; + return 'kelios sekundės'; } else { - return isFuture ? "kelių sekundžių" : "kelias sekundes"; + return isFuture ? 'kelių sekundžių' : 'kelias sekundes'; } } - function translateSingular(number, withoutSuffix, key, isFuture) { return withoutSuffix ? forms(key)[0] : (isFuture ? forms(key)[1] : forms(key)[2]); } - function special(number) { return number % 10 === 0 || (number > 10 && number < 20); } - function forms(key) { - return units[key].split("_"); + return units[key].split('_'); } - function translate(number, withoutSuffix, key, isFuture) { - var result = number + " "; + var result = number + ' '; if (number === 1) { return result + translateSingular(number, withoutSuffix, key[0], isFuture); } else if (withoutSuffix) { @@ -59,42 +52,42 @@ } } } - - function relativeWeekDay(moment, format) { - var nominative = format.indexOf('dddd HH:mm') === -1, - weekDay = weekDays[moment.day()]; - - return nominative ? weekDay : weekDay.substring(0, weekDay.length - 2) + "į"; - } - - return moment.defineLocale("lt", { - months : "sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio".split("_"), - monthsShort : "sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"), - weekdays : relativeWeekDay, - weekdaysShort : "Sek_Pir_Ant_Tre_Ket_Pen_Šeš".split("_"), - weekdaysMin : "S_P_A_T_K_Pn_Š".split("_"), + var lt = moment.defineLocale('lt', { + months : { + format: 'sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio'.split('_'), + standalone: 'sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis'.split('_') + }, + monthsShort : 'sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd'.split('_'), + weekdays : { + format: 'sekmadienį_pirmadienį_antradienį_trečiadienį_ketvirtadienį_penktadienį_šeštadienį'.split('_'), + standalone: 'sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis'.split('_'), + isFormat: /dddd HH:mm/ + }, + weekdaysShort : 'Sek_Pir_Ant_Tre_Ket_Pen_Šeš'.split('_'), + weekdaysMin : 'S_P_A_T_K_Pn_Š'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "YYYY-MM-DD", - LL : "YYYY [m.] MMMM D [d.]", - LLL : "YYYY [m.] MMMM D [d.], LT [val.]", - LLLL : "YYYY [m.] MMMM D [d.], dddd, LT [val.]", - l : "YYYY-MM-DD", - ll : "YYYY [m.] MMMM D [d.]", - lll : "YYYY [m.] MMMM D [d.], LT [val.]", - llll : "YYYY [m.] MMMM D [d.], ddd, LT [val.]" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'YYYY-MM-DD', + LL : 'YYYY [m.] MMMM D [d.]', + LLL : 'YYYY [m.] MMMM D [d.], HH:mm [val.]', + LLLL : 'YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]', + l : 'YYYY-MM-DD', + ll : 'YYYY [m.] MMMM D [d.]', + lll : 'YYYY [m.] MMMM D [d.], HH:mm [val.]', + llll : 'YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]' }, calendar : { - sameDay : "[Šiandien] LT", - nextDay : "[Rytoj] LT", - nextWeek : "dddd LT", - lastDay : "[Vakar] LT", - lastWeek : "[Praėjusį] dddd LT", - sameElse : "L" + sameDay : '[Šiandien] LT', + nextDay : '[Rytoj] LT', + nextWeek : 'dddd LT', + lastDay : '[Vakar] LT', + lastWeek : '[Praėjusį] dddd LT', + sameElse : 'L' }, relativeTime : { - future : "po %s", - past : "prieš %s", + future : 'po %s', + past : 'prieš %s', s : translateSeconds, m : translateSingular, mm : translate, @@ -107,6 +100,7 @@ y : translateSingular, yy : translate }, + ordinalParse: /\d{1,2}-oji/, ordinal : function (number) { return number + '-oji'; }, @@ -115,4 +109,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return lt; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/lv.js b/lib/javascripts/moment_locale/lv.js index 0df007d748..d1864a187e 100644 --- a/lib/javascripts/moment_locale/lv.js +++ b/lib/javascripts/moment_locale/lv.js @@ -1,49 +1,64 @@ -// moment.js locale configuration -// locale : latvian (lv) -// author : Kristaps Karlsons : https://github.com/skakri +//! moment.js locale configuration +//! locale : latvian (lv) +//! author : Kristaps Karlsons : https://github.com/skakri +//! author : Jānis Elmeris : https://github.com/JanisE + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var units = { - 'mm': 'minūti_minūtes_minūte_minūtes', - 'hh': 'stundu_stundas_stunda_stundas', - 'dd': 'dienu_dienas_diena_dienas', - 'MM': 'mēnesi_mēnešus_mēnesis_mēneši', - 'yy': 'gadu_gadus_gads_gadi' + 'm': 'minūtes_minūtēm_minūte_minūtes'.split('_'), + 'mm': 'minūtes_minūtēm_minūte_minūtes'.split('_'), + 'h': 'stundas_stundām_stunda_stundas'.split('_'), + 'hh': 'stundas_stundām_stunda_stundas'.split('_'), + 'd': 'dienas_dienām_diena_dienas'.split('_'), + 'dd': 'dienas_dienām_diena_dienas'.split('_'), + 'M': 'mēneša_mēnešiem_mēnesis_mēneši'.split('_'), + 'MM': 'mēneša_mēnešiem_mēnesis_mēneši'.split('_'), + 'y': 'gada_gadiem_gads_gadi'.split('_'), + 'yy': 'gada_gadiem_gads_gadi'.split('_') }; - - function format(word, number, withoutSuffix) { - var forms = word.split('_'); + /** + * @param withoutSuffix boolean true = a length of time; false = before/after a period of time. + */ + function format(forms, number, withoutSuffix) { if (withoutSuffix) { + // E.g. "21 minūte", "3 minūtes". return number % 10 === 1 && number !== 11 ? forms[2] : forms[3]; } else { + // E.g. "21 minūtes" as in "pēc 21 minūtes". + // E.g. "3 minūtēm" as in "pēc 3 minūtēm". return number % 10 === 1 && number !== 11 ? forms[0] : forms[1]; } } - function relativeTimeWithPlural(number, withoutSuffix, key) { return number + ' ' + format(units[key], number, withoutSuffix); } + function relativeTimeWithSingular(number, withoutSuffix, key) { + return format(units[key], number, withoutSuffix); + } + function relativeSeconds(number, withoutSuffix) { + return withoutSuffix ? 'dažas sekundes' : 'dažām sekundēm'; + } - return moment.defineLocale('lv', { - months : "janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris".split("_"), - monthsShort : "jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec".split("_"), - weekdays : "svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena".split("_"), - weekdaysShort : "Sv_P_O_T_C_Pk_S".split("_"), - weekdaysMin : "Sv_P_O_T_C_Pk_S".split("_"), + var lv = moment.defineLocale('lv', { + months : 'janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris'.split('_'), + monthsShort : 'jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec'.split('_'), + weekdays : 'svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena'.split('_'), + weekdaysShort : 'Sv_P_O_T_C_Pk_S'.split('_'), + weekdaysMin : 'Sv_P_O_T_C_Pk_S'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD.MM.YYYY", - LL : "YYYY. [gada] D. MMMM", - LLL : "YYYY. [gada] D. MMMM, LT", - LLLL : "YYYY. [gada] D. MMMM, dddd, LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY.', + LL : 'YYYY. [gada] D. MMMM', + LLL : 'YYYY. [gada] D. MMMM, HH:mm', + LLLL : 'YYYY. [gada] D. MMMM, dddd, HH:mm' }, calendar : { sameDay : '[Šodien pulksten] LT', @@ -54,24 +69,28 @@ sameElse : 'L' }, relativeTime : { - future : "%s vēlāk", - past : "%s agrāk", - s : "dažas sekundes", - m : "minūti", + future : 'pēc %s', + past : 'pirms %s', + s : relativeSeconds, + m : relativeTimeWithSingular, mm : relativeTimeWithPlural, - h : "stundu", + h : relativeTimeWithSingular, hh : relativeTimeWithPlural, - d : "dienu", + d : relativeTimeWithSingular, dd : relativeTimeWithPlural, - M : "mēnesi", + M : relativeTimeWithSingular, MM : relativeTimeWithPlural, - y : "gadu", + y : relativeTimeWithSingular, yy : relativeTimeWithPlural }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return lv; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/me.js b/lib/javascripts/moment_locale/me.js new file mode 100644 index 0000000000..e2c1f750e1 --- /dev/null +++ b/lib/javascripts/moment_locale/me.js @@ -0,0 +1,109 @@ +//! moment.js locale configuration +//! locale : Montenegrin (me) +//! author : Miodrag Nikač : https://github.com/miodragnikac + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var translator = { + words: { //Different grammatical cases + m: ['jedan minut', 'jednog minuta'], + mm: ['minut', 'minuta', 'minuta'], + h: ['jedan sat', 'jednog sata'], + hh: ['sat', 'sata', 'sati'], + dd: ['dan', 'dana', 'dana'], + MM: ['mjesec', 'mjeseca', 'mjeseci'], + yy: ['godina', 'godine', 'godina'] + }, + correctGrammaticalCase: function (number, wordKey) { + return number === 1 ? wordKey[0] : (number >= 2 && number <= 4 ? wordKey[1] : wordKey[2]); + }, + translate: function (number, withoutSuffix, key) { + var wordKey = translator.words[key]; + if (key.length === 1) { + return withoutSuffix ? wordKey[0] : wordKey[1]; + } else { + return number + ' ' + translator.correctGrammaticalCase(number, wordKey); + } + } + }; + + var me = moment.defineLocale('me', { + months: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'], + monthsShort: ['jan.', 'feb.', 'mar.', 'apr.', 'maj', 'jun', 'jul', 'avg.', 'sep.', 'okt.', 'nov.', 'dec.'], + weekdays: ['nedjelja', 'ponedjeljak', 'utorak', 'srijeda', 'četvrtak', 'petak', 'subota'], + weekdaysShort: ['ned.', 'pon.', 'uto.', 'sri.', 'čet.', 'pet.', 'sub.'], + weekdaysMin: ['ne', 'po', 'ut', 'sr', 'če', 'pe', 'su'], + longDateFormat: { + LT: 'H:mm', + LTS : 'H:mm:ss', + L: 'DD. MM. YYYY', + LL: 'D. MMMM YYYY', + LLL: 'D. MMMM YYYY H:mm', + LLLL: 'dddd, D. MMMM YYYY H:mm' + }, + calendar: { + sameDay: '[danas u] LT', + nextDay: '[sjutra u] LT', + + nextWeek: function () { + switch (this.day()) { + case 0: + return '[u] [nedjelju] [u] LT'; + case 3: + return '[u] [srijedu] [u] LT'; + case 6: + return '[u] [subotu] [u] LT'; + case 1: + case 2: + case 4: + case 5: + return '[u] dddd [u] LT'; + } + }, + lastDay : '[juče u] LT', + lastWeek : function () { + var lastWeekDays = [ + '[prošle] [nedjelje] [u] LT', + '[prošlog] [ponedjeljka] [u] LT', + '[prošlog] [utorka] [u] LT', + '[prošle] [srijede] [u] LT', + '[prošlog] [četvrtka] [u] LT', + '[prošlog] [petka] [u] LT', + '[prošle] [subote] [u] LT' + ]; + return lastWeekDays[this.day()]; + }, + sameElse : 'L' + }, + relativeTime : { + future : 'za %s', + past : 'prije %s', + s : 'nekoliko sekundi', + m : translator.translate, + mm : translator.translate, + h : translator.translate, + hh : translator.translate, + d : 'dan', + dd : translator.translate, + M : 'mjesec', + MM : translator.translate, + y : 'godinu', + yy : translator.translate + }, + ordinalParse: /\d{1,2}\./, + ordinal : '%d.', + week : { + dow : 1, // Monday is the first day of the week. + doy : 7 // The week that contains Jan 1st is the first week of the year. + } + }); + + return me; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/mk.js b/lib/javascripts/moment_locale/mk.js index 2d8a739abb..89b5414c11 100644 --- a/lib/javascripts/moment_locale/mk.js +++ b/lib/javascripts/moment_locale/mk.js @@ -1,64 +1,65 @@ -// moment.js locale configuration -// locale : macedonian (mk) -// author : Borislav Mickov : https://github.com/B0k0 +//! moment.js locale configuration +//! locale : macedonian (mk) +//! author : Borislav Mickov : https://github.com/B0k0 -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('mk', { - months : "јануари_февруари_март_април_мај_јуни_јули_август_септември_октомври_ноември_декември".split("_"), - monthsShort : "јан_фев_мар_апр_мај_јун_јул_авг_сеп_окт_ное_дек".split("_"), - weekdays : "недела_понеделник_вторник_среда_четврток_петок_сабота".split("_"), - weekdaysShort : "нед_пон_вто_сре_чет_пет_саб".split("_"), - weekdaysMin : "нe_пo_вт_ср_че_пе_сa".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var mk = moment.defineLocale('mk', { + months : 'јануари_февруари_март_април_мај_јуни_јули_август_септември_октомври_ноември_декември'.split('_'), + monthsShort : 'јан_фев_мар_апр_мај_јун_јул_авг_сеп_окт_ное_дек'.split('_'), + weekdays : 'недела_понеделник_вторник_среда_четврток_петок_сабота'.split('_'), + weekdaysShort : 'нед_пон_вто_сре_чет_пет_саб'.split('_'), + weekdaysMin : 'нe_пo_вт_ср_че_пе_сa'.split('_'), longDateFormat : { - LT : "H:mm", - L : "D.MM.YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'D.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY H:mm', + LLLL : 'dddd, D MMMM YYYY H:mm' }, calendar : { sameDay : '[Денес во] LT', nextDay : '[Утре во] LT', - nextWeek : 'dddd [во] LT', + nextWeek : '[Во] dddd [во] LT', lastDay : '[Вчера во] LT', lastWeek : function () { switch (this.day()) { case 0: case 3: case 6: - return '[Во изминатата] dddd [во] LT'; + return '[Изминатата] dddd [во] LT'; case 1: case 2: case 4: case 5: - return '[Во изминатиот] dddd [во] LT'; + return '[Изминатиот] dddd [во] LT'; } }, sameElse : 'L' }, relativeTime : { - future : "после %s", - past : "пред %s", - s : "неколку секунди", - m : "минута", - mm : "%d минути", - h : "час", - hh : "%d часа", - d : "ден", - dd : "%d дена", - M : "месец", - MM : "%d месеци", - y : "година", - yy : "%d години" + future : 'после %s', + past : 'пред %s', + s : 'неколку секунди', + m : 'минута', + mm : '%d минути', + h : 'час', + hh : '%d часа', + d : 'ден', + dd : '%d дена', + M : 'месец', + MM : '%d месеци', + y : 'година', + yy : '%d години' }, + ordinalParse: /\d{1,2}-(ев|ен|ти|ви|ри|ми)/, ordinal : function (number) { var lastDigit = number % 10, last2Digits = number % 100; @@ -83,4 +84,7 @@ doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return mk; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ml.js b/lib/javascripts/moment_locale/ml.js index d3cee1d3f3..9e241f2691 100644 --- a/lib/javascripts/moment_locale/ml.js +++ b/lib/javascripts/moment_locale/ml.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : malayalam (ml) -// author : Floyd Pink : https://github.com/floydpink +//! moment.js locale configuration +//! locale : malayalam (ml) +//! author : Floyd Pink : https://github.com/floydpink -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('ml', { - months : 'ജനുവരി_ഫെബ്രുവരി_മാർച്ച്_ഏപ്രിൽ_മേയ്_ജൂൺ_ജൂലൈ_ഓഗസ്റ്റ്_സെപ്റ്റംബർ_ഒക്ടോബർ_നവംബർ_ഡിസംബർ'.split("_"), - monthsShort : 'ജനു._ഫെബ്രു._മാർ._ഏപ്രി._മേയ്_ജൂൺ_ജൂലൈ._ഓഗ._സെപ്റ്റ._ഒക്ടോ._നവം._ഡിസം.'.split("_"), - weekdays : 'ഞായറാഴ്ച_തിങ്കളാഴ്ച_ചൊവ്വാഴ്ച_ബുധനാഴ്ച_വ്യാഴാഴ്ച_വെള്ളിയാഴ്ച_ശനിയാഴ്ച'.split("_"), - weekdaysShort : 'ഞായർ_തിങ്കൾ_ചൊവ്വ_ബുധൻ_വ്യാഴം_വെള്ളി_ശനി'.split("_"), - weekdaysMin : 'ഞാ_തി_ചൊ_ബു_വ്യാ_വെ_ശ'.split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var ml = moment.defineLocale('ml', { + months : 'ജനുവരി_ഫെബ്രുവരി_മാർച്ച്_ഏപ്രിൽ_മേയ്_ജൂൺ_ജൂലൈ_ഓഗസ്റ്റ്_സെപ്റ്റംബർ_ഒക്ടോബർ_നവംബർ_ഡിസംബർ'.split('_'), + monthsShort : 'ജനു._ഫെബ്രു._മാർ._ഏപ്രി._മേയ്_ജൂൺ_ജൂലൈ._ഓഗ._സെപ്റ്റ._ഒക്ടോ._നവം._ഡിസം.'.split('_'), + weekdays : 'ഞായറാഴ്ച_തിങ്കളാഴ്ച_ചൊവ്വാഴ്ച_ബുധനാഴ്ച_വ്യാഴാഴ്ച_വെള്ളിയാഴ്ച_ശനിയാഴ്ച'.split('_'), + weekdaysShort : 'ഞായർ_തിങ്കൾ_ചൊവ്വ_ബുധൻ_വ്യാഴം_വെള്ളി_ശനി'.split('_'), + weekdaysMin : 'ഞാ_തി_ചൊ_ബു_വ്യാ_വെ_ശ'.split('_'), longDateFormat : { - LT : "A h:mm -നു", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY, LT", - LLLL : "dddd, D MMMM YYYY, LT" + LT : 'A h:mm -നു', + LTS : 'A h:mm:ss -നു', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY, A h:mm -നു', + LLLL : 'dddd, D MMMM YYYY, A h:mm -നു' }, calendar : { sameDay : '[ഇന്ന്] LT', @@ -33,32 +33,39 @@ sameElse : 'L' }, relativeTime : { - future : "%s കഴിഞ്ഞ്", - past : "%s മുൻപ്", - s : "അൽപ നിമിഷങ്ങൾ", - m : "ഒരു മിനിറ്റ്", - mm : "%d മിനിറ്റ്", - h : "ഒരു മണിക്കൂർ", - hh : "%d മണിക്കൂർ", - d : "ഒരു ദിവസം", - dd : "%d ദിവസം", - M : "ഒരു മാസം", - MM : "%d മാസം", - y : "ഒരു വർഷം", - yy : "%d വർഷം" + future : '%s കഴിഞ്ഞ്', + past : '%s മുൻപ്', + s : 'അൽപ നിമിഷങ്ങൾ', + m : 'ഒരു മിനിറ്റ്', + mm : '%d മിനിറ്റ്', + h : 'ഒരു മണിക്കൂർ', + hh : '%d മണിക്കൂർ', + d : 'ഒരു ദിവസം', + dd : '%d ദിവസം', + M : 'ഒരു മാസം', + MM : '%d മാസം', + y : 'ഒരു വർഷം', + yy : '%d വർഷം' + }, + meridiemParse: /രാത്രി|രാവിലെ|ഉച്ച കഴിഞ്ഞ്|വൈകുന്നേരം|രാത്രി/i, + isPM : function (input) { + return /^(ഉച്ച കഴിഞ്ഞ്|വൈകുന്നേരം|രാത്രി)$/.test(input); }, meridiem : function (hour, minute, isLower) { if (hour < 4) { - return "രാത്രി"; + return 'രാത്രി'; } else if (hour < 12) { - return "രാവിലെ"; + return 'രാവിലെ'; } else if (hour < 17) { - return "ഉച്ച കഴിഞ്ഞ്"; + return 'ഉച്ച കഴിഞ്ഞ്'; } else if (hour < 20) { - return "വൈകുന്നേരം"; + return 'വൈകുന്നേരം'; } else { - return "രാത്രി"; + return 'രാത്രി'; } } }); -})); + + return ml; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/mr.js b/lib/javascripts/moment_locale/mr.js index 8cbfe7cf97..77a3fa2f06 100644 --- a/lib/javascripts/moment_locale/mr.js +++ b/lib/javascripts/moment_locale/mr.js @@ -1,16 +1,16 @@ -// moment.js locale configuration -// locale : Marathi (mr) -// author : Harshad Kale : https://github.com/kalehv +//! moment.js locale configuration +//! locale : Marathi (mr) +//! author : Harshad Kale : https://github.com/kalehv +//! author : Vivek Athalye : https://github.com/vnathalye + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var symbolMap = { '1': '१', '2': '२', @@ -36,18 +36,55 @@ '०': '0' }; - return moment.defineLocale('mr', { - months : 'जानेवारी_फेब्रुवारी_मार्च_एप्रिल_मे_जून_जुलै_ऑगस्ट_सप्टेंबर_ऑक्टोबर_नोव्हेंबर_डिसेंबर'.split("_"), - monthsShort: 'जाने._फेब्रु._मार्च._एप्रि._मे._जून._जुलै._ऑग._सप्टें._ऑक्टो._नोव्हें._डिसें.'.split("_"), - weekdays : 'रविवार_सोमवार_मंगळवार_बुधवार_गुरूवार_शुक्रवार_शनिवार'.split("_"), - weekdaysShort : 'रवि_सोम_मंगळ_बुध_गुरू_शुक्र_शनि'.split("_"), - weekdaysMin : 'र_सो_मं_बु_गु_शु_श'.split("_"), + function relativeTimeMr(number, withoutSuffix, string, isFuture) + { + var output = ''; + if (withoutSuffix) { + switch (string) { + case 's': output = 'काही सेकंद'; break; + case 'm': output = 'एक मिनिट'; break; + case 'mm': output = '%d मिनिटे'; break; + case 'h': output = 'एक तास'; break; + case 'hh': output = '%d तास'; break; + case 'd': output = 'एक दिवस'; break; + case 'dd': output = '%d दिवस'; break; + case 'M': output = 'एक महिना'; break; + case 'MM': output = '%d महिने'; break; + case 'y': output = 'एक वर्ष'; break; + case 'yy': output = '%d वर्षे'; break; + } + } + else { + switch (string) { + case 's': output = 'काही सेकंदां'; break; + case 'm': output = 'एका मिनिटा'; break; + case 'mm': output = '%d मिनिटां'; break; + case 'h': output = 'एका तासा'; break; + case 'hh': output = '%d तासां'; break; + case 'd': output = 'एका दिवसा'; break; + case 'dd': output = '%d दिवसां'; break; + case 'M': output = 'एका महिन्या'; break; + case 'MM': output = '%d महिन्यां'; break; + case 'y': output = 'एका वर्षा'; break; + case 'yy': output = '%d वर्षां'; break; + } + } + return output.replace(/%d/i, number); + } + + var mr = moment.defineLocale('mr', { + months : 'जानेवारी_फेब्रुवारी_मार्च_एप्रिल_मे_जून_जुलै_ऑगस्ट_सप्टेंबर_ऑक्टोबर_नोव्हेंबर_डिसेंबर'.split('_'), + monthsShort: 'जाने._फेब्रु._मार्च._एप्रि._मे._जून._जुलै._ऑग._सप्टें._ऑक्टो._नोव्हें._डिसें.'.split('_'), + weekdays : 'रविवार_सोमवार_मंगळवार_बुधवार_गुरूवार_शुक्रवार_शनिवार'.split('_'), + weekdaysShort : 'रवि_सोम_मंगळ_बुध_गुरू_शुक्र_शनि'.split('_'), + weekdaysMin : 'र_सो_मं_बु_गु_शु_श'.split('_'), longDateFormat : { - LT : "A h:mm वाजता", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY, LT", - LLLL : "dddd, D MMMM YYYY, LT" + LT : 'A h:mm वाजता', + LTS : 'A h:mm:ss वाजता', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY, A h:mm वाजता', + LLLL : 'dddd, D MMMM YYYY, A h:mm वाजता' }, calendar : { sameDay : '[आज] LT', @@ -58,19 +95,19 @@ sameElse : 'L' }, relativeTime : { - future : "%s नंतर", - past : "%s पूर्वी", - s : "सेकंद", - m: "एक मिनिट", - mm: "%d मिनिटे", - h : "एक तास", - hh : "%d तास", - d : "एक दिवस", - dd : "%d दिवस", - M : "एक महिना", - MM : "%d महिने", - y : "एक वर्ष", - yy : "%d वर्षे" + future: '%sमध्ये', + past: '%sपूर्वी', + s: relativeTimeMr, + m: relativeTimeMr, + mm: relativeTimeMr, + h: relativeTimeMr, + hh: relativeTimeMr, + d: relativeTimeMr, + dd: relativeTimeMr, + M: relativeTimeMr, + MM: relativeTimeMr, + y: relativeTimeMr, + yy: relativeTimeMr }, preparse: function (string) { return string.replace(/[१२३४५६७८९०]/g, function (match) { @@ -82,18 +119,32 @@ return symbolMap[match]; }); }, - meridiem: function (hour, minute, isLower) - { + meridiemParse: /रात्री|सकाळी|दुपारी|सायंकाळी/, + meridiemHour : function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === 'रात्री') { + return hour < 4 ? hour : hour + 12; + } else if (meridiem === 'सकाळी') { + return hour; + } else if (meridiem === 'दुपारी') { + return hour >= 10 ? hour : hour + 12; + } else if (meridiem === 'सायंकाळी') { + return hour + 12; + } + }, + meridiem: function (hour, minute, isLower) { if (hour < 4) { - return "रात्री"; + return 'रात्री'; } else if (hour < 10) { - return "सकाळी"; + return 'सकाळी'; } else if (hour < 17) { - return "दुपारी"; + return 'दुपारी'; } else if (hour < 20) { - return "सायंकाळी"; + return 'सायंकाळी'; } else { - return "रात्री"; + return 'रात्री'; } }, week : { @@ -101,4 +152,7 @@ doy : 6 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return mr; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ms-my.js b/lib/javascripts/moment_locale/ms-my.js index eee412f6bc..38c3138780 100644 --- a/lib/javascripts/moment_locale/ms-my.js +++ b/lib/javascripts/moment_locale/ms-my.js @@ -1,28 +1,41 @@ -// moment.js locale configuration -// locale : Bahasa Malaysia (ms-MY) -// author : Weldan Jamili : https://github.com/weldan +//! moment.js locale configuration +//! locale : Bahasa Malaysia (ms-MY) +//! author : Weldan Jamili : https://github.com/weldan -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('ms-my', { - months : "Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"), - monthsShort : "Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"), - weekdays : "Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"), - weekdaysShort : "Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"), - weekdaysMin : "Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var ms_my = moment.defineLocale('ms-my', { + months : 'Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember'.split('_'), + monthsShort : 'Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis'.split('_'), + weekdays : 'Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu'.split('_'), + weekdaysShort : 'Ahd_Isn_Sel_Rab_Kha_Jum_Sab'.split('_'), + weekdaysMin : 'Ah_Is_Sl_Rb_Km_Jm_Sb'.split('_'), longDateFormat : { - LT : "HH.mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY [pukul] LT", - LLLL : "dddd, D MMMM YYYY [pukul] LT" + LT : 'HH.mm', + LTS : 'HH.mm.ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY [pukul] HH.mm', + LLLL : 'dddd, D MMMM YYYY [pukul] HH.mm' + }, + meridiemParse: /pagi|tengahari|petang|malam/, + meridiemHour: function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === 'pagi') { + return hour; + } else if (meridiem === 'tengahari') { + return hour >= 11 ? hour : hour + 12; + } else if (meridiem === 'petang' || meridiem === 'malam') { + return hour + 12; + } }, meridiem : function (hours, minutes, isLower) { if (hours < 11) { @@ -44,23 +57,26 @@ sameElse : 'L' }, relativeTime : { - future : "dalam %s", - past : "%s yang lepas", - s : "beberapa saat", - m : "seminit", - mm : "%d minit", - h : "sejam", - hh : "%d jam", - d : "sehari", - dd : "%d hari", - M : "sebulan", - MM : "%d bulan", - y : "setahun", - yy : "%d tahun" + future : 'dalam %s', + past : '%s yang lepas', + s : 'beberapa saat', + m : 'seminit', + mm : '%d minit', + h : 'sejam', + hh : '%d jam', + d : 'sehari', + dd : '%d hari', + M : 'sebulan', + MM : '%d bulan', + y : 'setahun', + yy : '%d tahun' }, week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return ms_my; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ms.js b/lib/javascripts/moment_locale/ms.js new file mode 100644 index 0000000000..fbbb734d1d --- /dev/null +++ b/lib/javascripts/moment_locale/ms.js @@ -0,0 +1,82 @@ +//! moment.js locale configuration +//! locale : Bahasa Malaysia (ms-MY) +//! author : Weldan Jamili : https://github.com/weldan + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var ms = moment.defineLocale('ms', { + months : 'Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember'.split('_'), + monthsShort : 'Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis'.split('_'), + weekdays : 'Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu'.split('_'), + weekdaysShort : 'Ahd_Isn_Sel_Rab_Kha_Jum_Sab'.split('_'), + weekdaysMin : 'Ah_Is_Sl_Rb_Km_Jm_Sb'.split('_'), + longDateFormat : { + LT : 'HH.mm', + LTS : 'HH.mm.ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY [pukul] HH.mm', + LLLL : 'dddd, D MMMM YYYY [pukul] HH.mm' + }, + meridiemParse: /pagi|tengahari|petang|malam/, + meridiemHour: function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === 'pagi') { + return hour; + } else if (meridiem === 'tengahari') { + return hour >= 11 ? hour : hour + 12; + } else if (meridiem === 'petang' || meridiem === 'malam') { + return hour + 12; + } + }, + meridiem : function (hours, minutes, isLower) { + if (hours < 11) { + return 'pagi'; + } else if (hours < 15) { + return 'tengahari'; + } else if (hours < 19) { + return 'petang'; + } else { + return 'malam'; + } + }, + calendar : { + sameDay : '[Hari ini pukul] LT', + nextDay : '[Esok pukul] LT', + nextWeek : 'dddd [pukul] LT', + lastDay : '[Kelmarin pukul] LT', + lastWeek : 'dddd [lepas pukul] LT', + sameElse : 'L' + }, + relativeTime : { + future : 'dalam %s', + past : '%s yang lepas', + s : 'beberapa saat', + m : 'seminit', + mm : '%d minit', + h : 'sejam', + hh : '%d jam', + d : 'sehari', + dd : '%d hari', + M : 'sebulan', + MM : '%d bulan', + y : 'setahun', + yy : '%d tahun' + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 7 // The week that contains Jan 1st is the first week of the year. + } + }); + + return ms; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/my.js b/lib/javascripts/moment_locale/my.js index 442d5693c1..71e99f0ed8 100644 --- a/lib/javascripts/moment_locale/my.js +++ b/lib/javascripts/moment_locale/my.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : Burmese (my) -// author : Squar team, mysquar.com +//! moment.js locale configuration +//! locale : Burmese (my) +//! author : Squar team, mysquar.com + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var symbolMap = { '1': '၁', '2': '၂', @@ -34,18 +33,21 @@ '၉': '9', '၀': '0' }; - return moment.defineLocale('my', { - months: "ဇန်နဝါရီ_ဖေဖော်ဝါရီ_မတ်_ဧပြီ_မေ_ဇွန်_ဇူလိုင်_သြဂုတ်_စက်တင်ဘာ_အောက်တိုဘာ_နိုဝင်ဘာ_ဒီဇင်ဘာ".split("_"), - monthsShort: "ဇန်_ဖေ_မတ်_ပြီ_မေ_ဇွန်_လိုင်_သြ_စက်_အောက်_နို_ဒီ".split("_"), - weekdays: "တနင်္ဂနွေ_တနင်္လာ_အင်္ဂါ_ဗုဒ္ဓဟူး_ကြာသပတေး_သောကြာ_စနေ".split("_"), - weekdaysShort: "နွေ_လာ_င်္ဂါ_ဟူး_ကြာ_သော_နေ".split("_"), - weekdaysMin: "နွေ_လာ_င်္ဂါ_ဟူး_ကြာ_သော_နေ".split("_"), + + var my = moment.defineLocale('my', { + months: 'ဇန်နဝါရီ_ဖေဖော်ဝါရီ_မတ်_ဧပြီ_မေ_ဇွန်_ဇူလိုင်_သြဂုတ်_စက်တင်ဘာ_အောက်တိုဘာ_နိုဝင်ဘာ_ဒီဇင်ဘာ'.split('_'), + monthsShort: 'ဇန်_ဖေ_မတ်_ပြီ_မေ_ဇွန်_လိုင်_သြ_စက်_အောက်_နို_ဒီ'.split('_'), + weekdays: 'တနင်္ဂနွေ_တနင်္လာ_အင်္ဂါ_ဗုဒ္ဓဟူး_ကြာသပတေး_သောကြာ_စနေ'.split('_'), + weekdaysShort: 'နွေ_လာ_ဂါ_ဟူး_ကြာ_သော_နေ'.split('_'), + weekdaysMin: 'နွေ_လာ_ဂါ_ဟူး_ကြာ_သော_နေ'.split('_'), + longDateFormat: { - LT: "HH:mm", - L: "DD/MM/YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY LT", - LLLL: "dddd D MMMM YYYY LT" + LT: 'HH:mm', + LTS: 'HH:mm:ss', + L: 'DD/MM/YYYY', + LL: 'D MMMM YYYY', + LLL: 'D MMMM YYYY HH:mm', + LLLL: 'dddd D MMMM YYYY HH:mm' }, calendar: { sameDay: '[ယနေ.] LT [မှာ]', @@ -56,19 +58,19 @@ sameElse: 'L' }, relativeTime: { - future: "လာမည့် %s မှာ", - past: "လွန်ခဲ့သော %s က", - s: "စက္ကန်.အနည်းငယ်", - m: "တစ်မိနစ်", - mm: "%d မိနစ်", - h: "တစ်နာရီ", - hh: "%d နာရီ", - d: "တစ်ရက်", - dd: "%d ရက်", - M: "တစ်လ", - MM: "%d လ", - y: "တစ်နှစ်", - yy: "%d နှစ်" + future: 'လာမည့် %s မှာ', + past: 'လွန်ခဲ့သော %s က', + s: 'စက္ကန်.အနည်းငယ်', + m: 'တစ်မိနစ်', + mm: '%d မိနစ်', + h: 'တစ်နာရီ', + hh: '%d နာရီ', + d: 'တစ်ရက်', + dd: '%d ရက်', + M: 'တစ်လ', + MM: '%d လ', + y: 'တစ်နှစ်', + yy: '%d နှစ်' }, preparse: function (string) { return string.replace(/[၁၂၃၄၅၆၇၈၉၀]/g, function (match) { @@ -85,4 +87,7 @@ doy: 4 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return my; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/nb.js b/lib/javascripts/moment_locale/nb.js index 5e4a511a37..b8d76c6315 100644 --- a/lib/javascripts/moment_locale/nb.js +++ b/lib/javascripts/moment_locale/nb.js @@ -1,29 +1,29 @@ -// moment.js locale configuration -// locale : norwegian bokmål (nb) -// authors : Espen Hovlandsdal : https://github.com/rexxars -// Sigurd Gartmann : https://github.com/sigurdga +//! moment.js locale configuration +//! locale : norwegian bokmål (nb) +//! authors : Espen Hovlandsdal : https://github.com/rexxars +//! Sigurd Gartmann : https://github.com/sigurdga -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('nb', { - months : "januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"), - monthsShort : "jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"), - weekdays : "søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"), - weekdaysShort : "sø._ma._ti._on._to._fr._lø.".split("_"), - weekdaysMin : "sø_ma_ti_on_to_fr_lø".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var nb = moment.defineLocale('nb', { + months : 'januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember'.split('_'), + monthsShort : 'jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.'.split('_'), + weekdays : 'søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag'.split('_'), + weekdaysShort : 'sø._ma._ti._on._to._fr._lø.'.split('_'), + weekdaysMin : 'sø_ma_ti_on_to_fr_lø'.split('_'), longDateFormat : { - LT : "H.mm", - L : "DD.MM.YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY [kl.] LT", - LLLL : "dddd D. MMMM YYYY [kl.] LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY [kl.] HH:mm', + LLLL : 'dddd D. MMMM YYYY [kl.] HH:mm' }, calendar : { sameDay: '[i dag kl.] LT', @@ -34,24 +34,28 @@ sameElse: 'L' }, relativeTime : { - future : "om %s", - past : "for %s siden", - s : "noen sekunder", - m : "ett minutt", - mm : "%d minutter", - h : "en time", - hh : "%d timer", - d : "en dag", - dd : "%d dager", - M : "en måned", - MM : "%d måneder", - y : "ett år", - yy : "%d år" + future : 'om %s', + past : 'for %s siden', + s : 'noen sekunder', + m : 'ett minutt', + mm : '%d minutter', + h : 'en time', + hh : '%d timer', + d : 'en dag', + dd : '%d dager', + M : 'en måned', + MM : '%d måneder', + y : 'ett år', + yy : '%d år' }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return nb; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/nb_NO.js b/lib/javascripts/moment_locale/nb_NO.js deleted file mode 100644 index faa1aa2378..0000000000 --- a/lib/javascripts/moment_locale/nb_NO.js +++ /dev/null @@ -1,46 +0,0 @@ -// moment.js language configuration -// language : norwegian bokmål (nb) -// author : Espen Hovlandsdal : https://github.com/rexxars - -moment.lang('nb_NO', { - months : "januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"), - monthsShort : "jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"), - weekdays : "søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"), - weekdaysShort : "søn_man_tir_ons_tor_fre_lør".split("_"), - weekdaysMin : "sø_ma_ti_on_to_fr_lø".split("_"), - longDateFormat : { - LT : "HH:mm", - L : "YYYY-MM-DD", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" - }, - calendar : { - sameDay: '[I dag klokken] LT', - nextDay: '[I morgen klokken] LT', - nextWeek: 'dddd [klokken] LT', - lastDay: '[I går klokken] LT', - lastWeek: '[Forrige] dddd [klokken] LT', - sameElse: 'L' - }, - relativeTime : { - future : "om %s", - past : "for %s siden", - s : "noen sekunder", - m : "ett minutt", - mm : "%d minutter", - h : "en time", - hh : "%d timer", - d : "en dag", - dd : "%d dager", - M : "en måned", - MM : "%d måneder", - y : "ett år", - yy : "%d år" - }, - ordinal : '%d.', - week : { - dow : 1, // Monday is the first day of the week. - doy : 4 // The week that contains Jan 4th is the first week of the year. - } -}); diff --git a/lib/javascripts/moment_locale/ne.js b/lib/javascripts/moment_locale/ne.js index 836fb4d476..347640156b 100644 --- a/lib/javascripts/moment_locale/ne.js +++ b/lib/javascripts/moment_locale/ne.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : nepali/nepalese -// author : suvash : https://github.com/suvash +//! moment.js locale configuration +//! locale : nepali/nepalese +//! author : suvash : https://github.com/suvash + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var symbolMap = { '1': '१', '2': '२', @@ -36,18 +35,19 @@ '०': '0' }; - return moment.defineLocale('ne', { - months : 'जनवरी_फेब्रुवरी_मार्च_अप्रिल_मई_जुन_जुलाई_अगष्ट_सेप्टेम्बर_अक्टोबर_नोभेम्बर_डिसेम्बर'.split("_"), - monthsShort : 'जन._फेब्रु._मार्च_अप्रि._मई_जुन_जुलाई._अग._सेप्ट._अक्टो._नोभे._डिसे.'.split("_"), - weekdays : 'आइतबार_सोमबार_मङ्गलबार_बुधबार_बिहिबार_शुक्रबार_शनिबार'.split("_"), - weekdaysShort : 'आइत._सोम._मङ्गल._बुध._बिहि._शुक्र._शनि.'.split("_"), - weekdaysMin : 'आइ._सो._मङ्_बु._बि._शु._श.'.split("_"), + var ne = moment.defineLocale('ne', { + months : 'जनवरी_फेब्रुवरी_मार्च_अप्रिल_मई_जुन_जुलाई_अगष्ट_सेप्टेम्बर_अक्टोबर_नोभेम्बर_डिसेम्बर'.split('_'), + monthsShort : 'जन._फेब्रु._मार्च_अप्रि._मई_जुन_जुलाई._अग._सेप्ट._अक्टो._नोभे._डिसे.'.split('_'), + weekdays : 'आइतबार_सोमबार_मङ्गलबार_बुधबार_बिहिबार_शुक्रबार_शनिबार'.split('_'), + weekdaysShort : 'आइत._सोम._मङ्गल._बुध._बिहि._शुक्र._शनि.'.split('_'), + weekdaysMin : 'आ._सो._मं._बु._बि._शु._श.'.split('_'), longDateFormat : { - LT : "Aको h:mm बजे", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY, LT", - LLLL : "dddd, D MMMM YYYY, LT" + LT : 'Aको h:mm बजे', + LTS : 'Aको h:mm:ss बजे', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY, Aको h:mm बजे', + LLLL : 'dddd, D MMMM YYYY, Aको h:mm बजे' }, preparse: function (string) { return string.replace(/[१२३४५६७८९०]/g, function (match) { @@ -59,47 +59,63 @@ return symbolMap[match]; }); }, + meridiemParse: /राति|बिहान|दिउँसो|साँझ/, + meridiemHour : function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === 'राति') { + return hour < 4 ? hour : hour + 12; + } else if (meridiem === 'बिहान') { + return hour; + } else if (meridiem === 'दिउँसो') { + return hour >= 10 ? hour : hour + 12; + } else if (meridiem === 'साँझ') { + return hour + 12; + } + }, meridiem : function (hour, minute, isLower) { if (hour < 3) { - return "राती"; - } else if (hour < 10) { - return "बिहान"; - } else if (hour < 15) { - return "दिउँसो"; - } else if (hour < 18) { - return "बेलुका"; + return 'राति'; + } else if (hour < 12) { + return 'बिहान'; + } else if (hour < 16) { + return 'दिउँसो'; } else if (hour < 20) { - return "साँझ"; + return 'साँझ'; } else { - return "राती"; + return 'राति'; } }, calendar : { sameDay : '[आज] LT', - nextDay : '[भोली] LT', + nextDay : '[भोलि] LT', nextWeek : '[आउँदो] dddd[,] LT', lastDay : '[हिजो] LT', lastWeek : '[गएको] dddd[,] LT', sameElse : 'L' }, relativeTime : { - future : "%sमा", - past : "%s अगाडी", - s : "केही समय", - m : "एक मिनेट", - mm : "%d मिनेट", - h : "एक घण्टा", - hh : "%d घण्टा", - d : "एक दिन", - dd : "%d दिन", - M : "एक महिना", - MM : "%d महिना", - y : "एक बर्ष", - yy : "%d बर्ष" + future : '%sमा', + past : '%s अगाडि', + s : 'केही क्षण', + m : 'एक मिनेट', + mm : '%d मिनेट', + h : 'एक घण्टा', + hh : '%d घण्टा', + d : 'एक दिन', + dd : '%d दिन', + M : 'एक महिना', + MM : '%d महिना', + y : 'एक बर्ष', + yy : '%d बर्ष' }, week : { - dow : 1, // Monday is the first day of the week. - doy : 7 // The week that contains Jan 1st is the first week of the year. + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return ne; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/nl.js b/lib/javascripts/moment_locale/nl.js index 15776736ca..1ae4700d6a 100644 --- a/lib/javascripts/moment_locale/nl.js +++ b/lib/javascripts/moment_locale/nl.js @@ -1,21 +1,20 @@ -// moment.js locale configuration -// locale : dutch (nl) -// author : Joris Röling : https://github.com/jjupiter +//! moment.js locale configuration +//! locale : dutch (nl) +//! author : Joris Röling : https://github.com/jjupiter -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - var monthsShortWithDots = "jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"), - monthsShortWithoutDots = "jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"); +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; - return moment.defineLocale('nl', { - months : "januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"), + + var monthsShortWithDots = 'jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.'.split('_'), + monthsShortWithoutDots = 'jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec'.split('_'); + + var nl = moment.defineLocale('nl', { + months : 'januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december'.split('_'), monthsShort : function (m, format) { if (/-MMM-/.test(format)) { return monthsShortWithoutDots[m.month()]; @@ -23,15 +22,16 @@ return monthsShortWithDots[m.month()]; } }, - weekdays : "zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"), - weekdaysShort : "zo._ma._di._wo._do._vr._za.".split("_"), - weekdaysMin : "Zo_Ma_Di_Wo_Do_Vr_Za".split("_"), + weekdays : 'zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag'.split('_'), + weekdaysShort : 'zo._ma._di._wo._do._vr._za.'.split('_'), + weekdaysMin : 'Zo_Ma_Di_Wo_Do_Vr_Za'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD-MM-YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD-MM-YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' }, calendar : { sameDay: '[vandaag om] LT', @@ -42,20 +42,21 @@ sameElse: 'L' }, relativeTime : { - future : "over %s", - past : "%s geleden", - s : "een paar seconden", - m : "één minuut", - mm : "%d minuten", - h : "één uur", - hh : "%d uur", - d : "één dag", - dd : "%d dagen", - M : "één maand", - MM : "%d maanden", - y : "één jaar", - yy : "%d jaar" + future : 'over %s', + past : '%s geleden', + s : 'een paar seconden', + m : 'één minuut', + mm : '%d minuten', + h : 'één uur', + hh : '%d uur', + d : 'één dag', + dd : '%d dagen', + M : 'één maand', + MM : '%d maanden', + y : 'één jaar', + yy : '%d jaar' }, + ordinalParse: /\d{1,2}(ste|de)/, ordinal : function (number) { return number + ((number === 1 || number === 8 || number >= 20) ? 'ste' : 'de'); }, @@ -64,4 +65,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return nl; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/nn.js b/lib/javascripts/moment_locale/nn.js index e479b4582c..3910bb9be6 100644 --- a/lib/javascripts/moment_locale/nn.js +++ b/lib/javascripts/moment_locale/nn.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : norwegian nynorsk (nn) -// author : https://github.com/mechuwind +//! moment.js locale configuration +//! locale : norwegian nynorsk (nn) +//! author : https://github.com/mechuwind -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('nn', { - months : "januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"), - monthsShort : "jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"), - weekdays : "sundag_måndag_tysdag_onsdag_torsdag_fredag_laurdag".split("_"), - weekdaysShort : "sun_mån_tys_ons_tor_fre_lau".split("_"), - weekdaysMin : "su_må_ty_on_to_fr_lø".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var nn = moment.defineLocale('nn', { + months : 'januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember'.split('_'), + monthsShort : 'jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des'.split('_'), + weekdays : 'sundag_måndag_tysdag_onsdag_torsdag_fredag_laurdag'.split('_'), + weekdaysShort : 'sun_mån_tys_ons_tor_fre_lau'.split('_'), + weekdaysMin : 'su_må_ty_on_to_fr_lø'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD.MM.YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY [kl.] H:mm', + LLLL : 'dddd D. MMMM YYYY [kl.] HH:mm' }, calendar : { sameDay: '[I dag klokka] LT', @@ -33,24 +33,28 @@ sameElse: 'L' }, relativeTime : { - future : "om %s", - past : "for %s sidan", - s : "nokre sekund", - m : "eit minutt", - mm : "%d minutt", - h : "ein time", - hh : "%d timar", - d : "ein dag", - dd : "%d dagar", - M : "ein månad", - MM : "%d månader", - y : "eit år", - yy : "%d år" + future : 'om %s', + past : 'for %s sidan', + s : 'nokre sekund', + m : 'eit minutt', + mm : '%d minutt', + h : 'ein time', + hh : '%d timar', + d : 'ein dag', + dd : '%d dagar', + M : 'ein månad', + MM : '%d månader', + y : 'eit år', + yy : '%d år' }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return nn; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/pl.js b/lib/javascripts/moment_locale/pl.js index 75e978bd23..fa8157943b 100644 --- a/lib/javascripts/moment_locale/pl.js +++ b/lib/javascripts/moment_locale/pl.js @@ -1,25 +1,22 @@ -// moment.js locale configuration -// locale : polish (pl) -// author : Rafal Hirsz : https://github.com/evoL +//! moment.js locale configuration +//! locale : polish (pl) +//! author : Rafal Hirsz : https://github.com/evoL -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - var monthsNominative = "styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"), - monthsSubjective = "stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_"); +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + var monthsNominative = 'styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień'.split('_'), + monthsSubjective = 'stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia'.split('_'); function plural(n) { return (n % 10 < 5) && (n % 10 > 1) && ((~~(n / 10) % 10) !== 1); } - function translate(number, withoutSuffix, key) { - var result = number + " "; + var result = number + ' '; switch (key) { case 'm': return withoutSuffix ? 'minuta' : 'minutę'; @@ -36,24 +33,30 @@ } } - return moment.defineLocale('pl', { + var pl = moment.defineLocale('pl', { months : function (momentToFormat, format) { - if (/D MMMM/.test(format)) { + if (format === '') { + // Hack: if format empty we know this is used to generate + // RegExp by moment. Give then back both valid forms of months + // in RegExp ready format. + return '(' + monthsSubjective[momentToFormat.month()] + '|' + monthsNominative[momentToFormat.month()] + ')'; + } else if (/D MMMM/.test(format)) { return monthsSubjective[momentToFormat.month()]; } else { return monthsNominative[momentToFormat.month()]; } }, - monthsShort : "sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"), - weekdays : "niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"), - weekdaysShort : "nie_pon_wt_śr_czw_pt_sb".split("_"), - weekdaysMin : "N_Pn_Wt_Śr_Cz_Pt_So".split("_"), + monthsShort : 'sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru'.split('_'), + weekdays : 'niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota'.split('_'), + weekdaysShort : 'nie_pon_wt_śr_czw_pt_sb'.split('_'), + weekdaysMin : 'Nd_Pn_Wt_Śr_Cz_Pt_So'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD.MM.YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' }, calendar : { sameDay: '[Dziś o] LT', @@ -75,24 +78,28 @@ sameElse: 'L' }, relativeTime : { - future : "za %s", - past : "%s temu", - s : "kilka sekund", + future : 'za %s', + past : '%s temu', + s : 'kilka sekund', m : translate, mm : translate, h : translate, hh : translate, - d : "1 dzień", + d : '1 dzień', dd : '%d dni', - M : "miesiąc", + M : 'miesiąc', MM : translate, - y : "rok", + y : 'rok', yy : translate }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return pl; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/pt-br.js b/lib/javascripts/moment_locale/pt-br.js index d577018fac..06747f6fda 100644 --- a/lib/javascripts/moment_locale/pt-br.js +++ b/lib/javascripts/moment_locale/pt-br.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : brazilian portuguese (pt-br) -// author : Caio Ribeiro Pereira : https://github.com/caio-ribeiro-pereira +//! moment.js locale configuration +//! locale : brazilian portuguese (pt-br) +//! author : Caio Ribeiro Pereira : https://github.com/caio-ribeiro-pereira -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('pt-br', { - months : "janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"), - monthsShort : "jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"), - weekdays : "domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"), - weekdaysShort : "dom_seg_ter_qua_qui_sex_sáb".split("_"), - weekdaysMin : "dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var pt_br = moment.defineLocale('pt-br', { + months : 'Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro'.split('_'), + monthsShort : 'Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez'.split('_'), + weekdays : 'Domingo_Segunda-Feira_Terça-Feira_Quarta-Feira_Quinta-Feira_Sexta-Feira_Sábado'.split('_'), + weekdaysShort : 'Dom_Seg_Ter_Qua_Qui_Sex_Sáb'.split('_'), + weekdaysMin : 'Dom_2ª_3ª_4ª_5ª_6ª_Sáb'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D [de] MMMM [de] YYYY", - LLL : "D [de] MMMM [de] YYYY [às] LT", - LLLL : "dddd, D [de] MMMM [de] YYYY [às] LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D [de] MMMM [de] YYYY', + LLL : 'D [de] MMMM [de] YYYY [às] HH:mm', + LLLL : 'dddd, D [de] MMMM [de] YYYY [às] HH:mm' }, calendar : { sameDay: '[Hoje às] LT', @@ -37,20 +37,24 @@ sameElse: 'L' }, relativeTime : { - future : "em %s", - past : "%s atrás", - s : "segundos", - m : "um minuto", - mm : "%d minutos", - h : "uma hora", - hh : "%d horas", - d : "um dia", - dd : "%d dias", - M : "um mês", - MM : "%d meses", - y : "um ano", - yy : "%d anos" + future : 'em %s', + past : '%s atrás', + s : 'poucos segundos', + m : 'um minuto', + mm : '%d minutos', + h : 'uma hora', + hh : '%d horas', + d : 'um dia', + dd : '%d dias', + M : 'um mês', + MM : '%d meses', + y : 'um ano', + yy : '%d anos' }, + ordinalParse: /\d{1,2}º/, ordinal : '%dº' }); -})); + + return pt_br; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/pt.js b/lib/javascripts/moment_locale/pt.js index 808641483d..bdb913fc0f 100644 --- a/lib/javascripts/moment_locale/pt.js +++ b/lib/javascripts/moment_locale/pt.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : portuguese (pt) -// author : Jefferson : https://github.com/jalex79 +//! moment.js locale configuration +//! locale : portuguese (pt) +//! author : Jefferson : https://github.com/jalex79 -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('pt', { - months : "janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"), - monthsShort : "jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"), - weekdays : "domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"), - weekdaysShort : "dom_seg_ter_qua_qui_sex_sáb".split("_"), - weekdaysMin : "dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var pt = moment.defineLocale('pt', { + months : 'Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro'.split('_'), + monthsShort : 'Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez'.split('_'), + weekdays : 'Domingo_Segunda-Feira_Terça-Feira_Quarta-Feira_Quinta-Feira_Sexta-Feira_Sábado'.split('_'), + weekdaysShort : 'Dom_Seg_Ter_Qua_Qui_Sex_Sáb'.split('_'), + weekdaysMin : 'Dom_2ª_3ª_4ª_5ª_6ª_Sáb'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D [de] MMMM [de] YYYY", - LLL : "D [de] MMMM [de] YYYY LT", - LLLL : "dddd, D [de] MMMM [de] YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D [de] MMMM [de] YYYY', + LLL : 'D [de] MMMM [de] YYYY HH:mm', + LLLL : 'dddd, D [de] MMMM [de] YYYY HH:mm' }, calendar : { sameDay: '[Hoje às] LT', @@ -37,24 +37,28 @@ sameElse: 'L' }, relativeTime : { - future : "em %s", - past : "há %s", - s : "segundos", - m : "um minuto", - mm : "%d minutos", - h : "uma hora", - hh : "%d horas", - d : "um dia", - dd : "%d dias", - M : "um mês", - MM : "%d meses", - y : "um ano", - yy : "%d anos" + future : 'em %s', + past : 'há %s', + s : 'segundos', + m : 'um minuto', + mm : '%d minutos', + h : 'uma hora', + hh : '%d horas', + d : 'um dia', + dd : '%d dias', + M : 'um mês', + MM : '%d meses', + y : 'um ano', + yy : '%d anos' }, + ordinalParse: /\d{1,2}º/, ordinal : '%dº', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return pt; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/pt_BR.js b/lib/javascripts/moment_locale/pt_BR.js deleted file mode 100644 index 5ffc39ee58..0000000000 --- a/lib/javascripts/moment_locale/pt_BR.js +++ /dev/null @@ -1,46 +0,0 @@ -// moment.js language configuration -// language : brazilian portuguese (pt-br) -// author : Caio Ribeiro Pereira : https://github.com/caio-ribeiro-pereira - -moment.lang('pt_BR', { - months : "Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro".split("_"), - monthsShort : "Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"), - weekdays : "Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado".split("_"), - weekdaysShort : "Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"), - weekdaysMin : "Dom_2ª_3ª_4ª_5ª_6ª_Sáb".split("_"), - longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D [de] MMMM [de] YYYY", - LLL : "D [de] MMMM [de] YYYY LT", - LLLL : "dddd, D [de] MMMM [de] YYYY LT" - }, - calendar : { - sameDay: '[Hoje às] LT', - nextDay: '[Amanhã às] LT', - nextWeek: 'dddd [às] LT', - lastDay: '[Ontem às] LT', - lastWeek: function () { - return (this.day() === 0 || this.day() === 6) ? - '[Último] dddd [às] LT' : // Saturday + Sunday - '[Última] dddd [às] LT'; // Monday - Friday - }, - sameElse: 'L' - }, - relativeTime : { - future : "em %s", - past : "%s atrás", - s : "segundos", - m : "um minuto", - mm : "%d minutos", - h : "uma hora", - hh : "%d horas", - d : "um dia", - dd : "%d dias", - M : "um mês", - MM : "%d meses", - y : "um ano", - yy : "%d anos" - }, - ordinal : '%dº' -}); diff --git a/lib/javascripts/moment_locale/ro.js b/lib/javascripts/moment_locale/ro.js index 21a3293191..fb9226758c 100644 --- a/lib/javascripts/moment_locale/ro.js +++ b/lib/javascripts/moment_locale/ro.js @@ -1,17 +1,16 @@ -// moment.js locale configuration -// locale : romanian (ro) -// author : Vlad Gurdiga : https://github.com/gurdiga -// author : Valentin Agachi : https://github.com/avaly +//! moment.js locale configuration +//! locale : romanian (ro) +//! author : Vlad Gurdiga : https://github.com/gurdiga +//! author : Valentin Agachi : https://github.com/avaly + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function relativeTimeWithPlural(number, withoutSuffix, key) { var format = { 'mm': 'minute', @@ -24,25 +23,25 @@ if (number % 100 >= 20 || (number >= 100 && number % 100 === 0)) { separator = ' de '; } - return number + separator + format[key]; } - return moment.defineLocale('ro', { - months : "ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"), - monthsShort : "ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"), - weekdays : "duminică_luni_marți_miercuri_joi_vineri_sâmbătă".split("_"), - weekdaysShort : "Dum_Lun_Mar_Mie_Joi_Vin_Sâm".split("_"), - weekdaysMin : "Du_Lu_Ma_Mi_Jo_Vi_Sâ".split("_"), + var ro = moment.defineLocale('ro', { + months : 'ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie'.split('_'), + monthsShort : 'ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.'.split('_'), + weekdays : 'duminică_luni_marți_miercuri_joi_vineri_sâmbătă'.split('_'), + weekdaysShort : 'Dum_Lun_Mar_Mie_Joi_Vin_Sâm'.split('_'), + weekdaysMin : 'Du_Lu_Ma_Mi_Jo_Vi_Sâ'.split('_'), longDateFormat : { - LT : "H:mm", - L : "DD.MM.YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY H:mm", - LLLL : "dddd, D MMMM YYYY H:mm" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY H:mm', + LLLL : 'dddd, D MMMM YYYY H:mm' }, calendar : { - sameDay: "[azi la] LT", + sameDay: '[azi la] LT', nextDay: '[mâine la] LT', nextWeek: 'dddd [la] LT', lastDay: '[ieri la] LT', @@ -50,18 +49,18 @@ sameElse: 'L' }, relativeTime : { - future : "peste %s", - past : "%s în urmă", - s : "câteva secunde", - m : "un minut", + future : 'peste %s', + past : '%s în urmă', + s : 'câteva secunde', + m : 'un minut', mm : relativeTimeWithPlural, - h : "o oră", + h : 'o oră', hh : relativeTimeWithPlural, - d : "o zi", + d : 'o zi', dd : relativeTimeWithPlural, - M : "o lună", + M : 'o lună', MM : relativeTimeWithPlural, - y : "un an", + y : 'un an', yy : relativeTimeWithPlural }, week : { @@ -69,4 +68,7 @@ doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return ro; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ru.js b/lib/javascripts/moment_locale/ru.js index 3ae8d23b7c..442ddf8003 100644 --- a/lib/javascripts/moment_locale/ru.js +++ b/lib/javascripts/moment_locale/ru.js @@ -1,22 +1,20 @@ -// moment.js locale configuration -// locale : russian (ru) -// author : Viktorminator : https://github.com/Viktorminator -// Author : Menelion Elensúle : https://github.com/Oire +//! moment.js locale configuration +//! locale : russian (ru) +//! author : Viktorminator : https://github.com/Viktorminator +//! Author : Menelion Elensúle : https://github.com/Oire + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function plural(word, num) { var forms = word.split('_'); return num % 10 === 1 && num % 100 !== 11 ? forms[0] : (num % 10 >= 2 && num % 10 <= 4 && (num % 100 < 10 || num % 100 >= 20) ? forms[1] : forms[2]); } - function relativeTimeWithPlural(number, withoutSuffix, key) { var format = { 'mm': withoutSuffix ? 'минута_минуты_минут' : 'минуту_минуты_минут', @@ -32,116 +30,116 @@ return number + ' ' + plural(format[key], +number); } } + var monthsParse = [/^янв/i, /^фев/i, /^мар/i, /^апр/i, /^ма[й|я]/i, /^июн/i, /^июл/i, /^авг/i, /^сен/i, /^окт/i, /^ноя/i, /^дек/i]; - function monthsCaseReplace(m, format) { - var months = { - 'nominative': 'январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь'.split('_'), - 'accusative': 'января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря'.split('_') + var ru = moment.defineLocale('ru', { + months : { + format: 'Января_Февраля_Марта_Апреля_Мая_Июня_Июля_Августа_Сентября_Октября_Ноября_Декабря'.split('_'), + standalone: 'Январь_Февраль_Март_Апрель_Май_Июнь_Июль_Август_Сентябрь_Октябрь_Ноябрь_Декабрь'.split('_') }, - - nounCase = (/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/).test(format) ? - 'accusative' : - 'nominative'; - - return months[nounCase][m.month()]; - } - - function monthsShortCaseReplace(m, format) { - var monthsShort = { - 'nominative': 'янв_фев_мар_апр_май_июнь_июль_авг_сен_окт_ноя_дек'.split('_'), - 'accusative': 'янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек'.split('_') + monthsShort : { + format: 'янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек'.split('_'), + standalone: 'янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек'.split('_') }, - - nounCase = (/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/).test(format) ? - 'accusative' : - 'nominative'; - - return monthsShort[nounCase][m.month()]; - } - - function weekdaysCaseReplace(m, format) { - var weekdays = { - 'nominative': 'воскресенье_понедельник_вторник_среда_четверг_пятница_суббота'.split('_'), - 'accusative': 'воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу'.split('_') + weekdays : { + standalone: 'Воскресенье_Понедельник_Вторник_Среда_Четверг_Пятница_Суббота'.split('_'), + format: 'Воскресенье_Понедельник_Вторник_Среду_Четверг_Пятницу_Субботу'.split('_'), + isFormat: /\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/ }, - - nounCase = (/\[ ?[Вв] ?(?:прошлую|следующую)? ?\] ?dddd/).test(format) ? - 'accusative' : - 'nominative'; - - return weekdays[nounCase][m.day()]; - } - - return moment.defineLocale('ru', { - months : monthsCaseReplace, - monthsShort : monthsShortCaseReplace, - weekdays : weekdaysCaseReplace, - weekdaysShort : "вс_пн_вт_ср_чт_пт_сб".split("_"), - weekdaysMin : "вс_пн_вт_ср_чт_пт_сб".split("_"), - monthsParse : [/^янв/i, /^фев/i, /^мар/i, /^апр/i, /^ма[й|я]/i, /^июн/i, /^июл/i, /^авг/i, /^сен/i, /^окт/i, /^ноя/i, /^дек/i], + weekdaysShort : 'Вс_Пн_Вт_Ср_Чт_Пт_Сб'.split('_'), + weekdaysMin : 'Вс_Пн_Вт_Ср_Чт_Пт_Сб'.split('_'), + monthsParse : monthsParse, + longMonthsParse : monthsParse, + shortMonthsParse : monthsParse, longDateFormat : { - LT : "HH:mm", - L : "DD.MM.YYYY", - LL : "D MMMM YYYY г.", - LLL : "D MMMM YYYY г., LT", - LLLL : "dddd, D MMMM YYYY г., LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY г.', + LLL : 'D MMMM YYYY г., HH:mm', + LLLL : 'dddd, D MMMM YYYY г., HH:mm' }, calendar : { sameDay: '[Сегодня в] LT', nextDay: '[Завтра в] LT', lastDay: '[Вчера в] LT', - nextWeek: function () { - return this.day() === 2 ? '[Во] dddd [в] LT' : '[В] dddd [в] LT'; + nextWeek: function (now) { + if (now.week() !== this.week()) { + switch (this.day()) { + case 0: + return '[В следующее] dddd [в] LT'; + case 1: + case 2: + case 4: + return '[В следующий] dddd [в] LT'; + case 3: + case 5: + case 6: + return '[В следующую] dddd [в] LT'; + } + } else { + if (this.day() === 2) { + return '[Во] dddd [в] LT'; + } else { + return '[В] dddd [в] LT'; + } + } }, - lastWeek: function () { - switch (this.day()) { - case 0: - return '[В прошлое] dddd [в] LT'; - case 1: - case 2: - case 4: - return '[В прошлый] dddd [в] LT'; - case 3: - case 5: - case 6: - return '[В прошлую] dddd [в] LT'; + lastWeek: function (now) { + if (now.week() !== this.week()) { + switch (this.day()) { + case 0: + return '[В прошлое] dddd [в] LT'; + case 1: + case 2: + case 4: + return '[В прошлый] dddd [в] LT'; + case 3: + case 5: + case 6: + return '[В прошлую] dddd [в] LT'; + } + } else { + if (this.day() === 2) { + return '[Во] dddd [в] LT'; + } else { + return '[В] dddd [в] LT'; + } } }, sameElse: 'L' }, relativeTime : { - future : "через %s", - past : "%s назад", - s : "несколько секунд", + future : 'через %s', + past : '%s назад', + s : 'несколько секунд', m : relativeTimeWithPlural, mm : relativeTimeWithPlural, - h : "час", + h : 'час', hh : relativeTimeWithPlural, - d : "день", + d : 'день', dd : relativeTimeWithPlural, - M : "месяц", + M : 'месяц', MM : relativeTimeWithPlural, - y : "год", + y : 'год', yy : relativeTimeWithPlural }, - meridiemParse: /ночи|утра|дня|вечера/i, isPM : function (input) { return /^(дня|вечера)$/.test(input); }, - meridiem : function (hour, minute, isLower) { if (hour < 4) { - return "ночи"; + return 'ночи'; } else if (hour < 12) { - return "утра"; + return 'утра'; } else if (hour < 17) { - return "дня"; + return 'дня'; } else { - return "вечера"; + return 'вечера'; } }, - + ordinalParse: /\d{1,2}-(й|го|я)/, ordinal: function (number, period) { switch (period) { case 'M': @@ -157,10 +155,12 @@ return number; } }, - week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return ru; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/se.js b/lib/javascripts/moment_locale/se.js new file mode 100644 index 0000000000..06be0d6ba1 --- /dev/null +++ b/lib/javascripts/moment_locale/se.js @@ -0,0 +1,61 @@ +//! moment.js locale configuration +//! locale : Northern Sami (se) +//! authors : Bård Rolstad Henriksen : https://github.com/karamell + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + + var se = moment.defineLocale('se', { + months : 'ođđajagemánnu_guovvamánnu_njukčamánnu_cuoŋománnu_miessemánnu_geassemánnu_suoidnemánnu_borgemánnu_čakčamánnu_golggotmánnu_skábmamánnu_juovlamánnu'.split('_'), + monthsShort : 'ođđj_guov_njuk_cuo_mies_geas_suoi_borg_čakč_golg_skáb_juov'.split('_'), + weekdays : 'sotnabeaivi_vuossárga_maŋŋebárga_gaskavahkku_duorastat_bearjadat_lávvardat'.split('_'), + weekdaysShort : 'sotn_vuos_maŋ_gask_duor_bear_láv'.split('_'), + weekdaysMin : 's_v_m_g_d_b_L'.split('_'), + longDateFormat : { + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'MMMM D. [b.] YYYY', + LLL : 'MMMM D. [b.] YYYY [ti.] HH:mm', + LLLL : 'dddd, MMMM D. [b.] YYYY [ti.] HH:mm' + }, + calendar : { + sameDay: '[otne ti] LT', + nextDay: '[ihttin ti] LT', + nextWeek: 'dddd [ti] LT', + lastDay: '[ikte ti] LT', + lastWeek: '[ovddit] dddd [ti] LT', + sameElse: 'L' + }, + relativeTime : { + future : '%s geažes', + past : 'maŋit %s', + s : 'moadde sekunddat', + m : 'okta minuhta', + mm : '%d minuhtat', + h : 'okta diimmu', + hh : '%d diimmut', + d : 'okta beaivi', + dd : '%d beaivvit', + M : 'okta mánnu', + MM : '%d mánut', + y : 'okta jahki', + yy : '%d jagit' + }, + ordinalParse: /\d{1,2}\./, + ordinal : '%d.', + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } + }); + + return se; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/si.js b/lib/javascripts/moment_locale/si.js new file mode 100644 index 0000000000..d86c9e3b8e --- /dev/null +++ b/lib/javascripts/moment_locale/si.js @@ -0,0 +1,66 @@ +//! moment.js locale configuration +//! locale : Sinhalese (si) +//! author : Sampath Sitinamaluwa : https://github.com/sampathsris + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + /*jshint -W100*/ + var si = moment.defineLocale('si', { + months : 'ජනවාරි_පෙබරවාරි_මාර්තු_අප්‍රේල්_මැයි_ජූනි_ජූලි_අගෝස්තු_සැප්තැම්බර්_ඔක්තෝබර්_නොවැම්බර්_දෙසැම්බර්'.split('_'), + monthsShort : 'ජන_පෙබ_මාර්_අප්_මැයි_ජූනි_ජූලි_අගෝ_සැප්_ඔක්_නොවැ_දෙසැ'.split('_'), + weekdays : 'ඉරිදා_සඳුදා_අඟහරුවාදා_බදාදා_බ්‍රහස්පතින්දා_සිකුරාදා_සෙනසුරාදා'.split('_'), + weekdaysShort : 'ඉරි_සඳු_අඟ_බදා_බ්‍රහ_සිකු_සෙන'.split('_'), + weekdaysMin : 'ඉ_ස_අ_බ_බ්‍ර_සි_සෙ'.split('_'), + longDateFormat : { + LT : 'a h:mm', + LTS : 'a h:mm:ss', + L : 'YYYY/MM/DD', + LL : 'YYYY MMMM D', + LLL : 'YYYY MMMM D, a h:mm', + LLLL : 'YYYY MMMM D [වැනි] dddd, a h:mm:ss' + }, + calendar : { + sameDay : '[අද] LT[ට]', + nextDay : '[හෙට] LT[ට]', + nextWeek : 'dddd LT[ට]', + lastDay : '[ඊයේ] LT[ට]', + lastWeek : '[පසුගිය] dddd LT[ට]', + sameElse : 'L' + }, + relativeTime : { + future : '%sකින්', + past : '%sකට පෙර', + s : 'තත්පර කිහිපය', + m : 'මිනිත්තුව', + mm : 'මිනිත්තු %d', + h : 'පැය', + hh : 'පැය %d', + d : 'දිනය', + dd : 'දින %d', + M : 'මාසය', + MM : 'මාස %d', + y : 'වසර', + yy : 'වසර %d' + }, + ordinalParse: /\d{1,2} වැනි/, + ordinal : function (number) { + return number + ' වැනි'; + }, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'ප.ව.' : 'පස් වරු'; + } else { + return isLower ? 'පෙ.ව.' : 'පෙර වරු'; + } + } + }); + + return si; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/sk.js b/lib/javascripts/moment_locale/sk.js index d03fff875b..52838a3a62 100644 --- a/lib/javascripts/moment_locale/sk.js +++ b/lib/javascripts/moment_locale/sk.js @@ -1,26 +1,23 @@ -// moment.js locale configuration -// locale : slovak (sk) -// author : Martin Minka : https://github.com/k2s -// based on work of petrbela : https://github.com/petrbela +//! moment.js locale configuration +//! locale : slovak (sk) +//! author : Martin Minka : https://github.com/k2s +//! based on work of petrbela : https://github.com/petrbela -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - var months = "január_február_marec_apríl_máj_jún_júl_august_september_október_november_december".split("_"), - monthsShort = "jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec".split("_"); +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + var months = 'január_február_marec_apríl_máj_jún_júl_august_september_október_november_december'.split('_'), + monthsShort = 'jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec'.split('_'); function plural(n) { return (n > 1) && (n < 5); } - function translate(number, withoutSuffix, key, isFuture) { - var result = number + " "; + var result = number + ' '; switch (key) { case 's': // a few seconds / in a few seconds / a few seconds ago return (withoutSuffix || isFuture) ? 'pár sekúnd' : 'pár sekundami'; @@ -72,29 +69,22 @@ } } - return moment.defineLocale('sk', { + var sk = moment.defineLocale('sk', { months : months, monthsShort : monthsShort, - monthsParse : (function (months, monthsShort) { - var i, _monthsParse = []; - for (i = 0; i < 12; i++) { - // use custom parser to solve problem with July (červenec) - _monthsParse[i] = new RegExp('^' + months[i] + '$|^' + monthsShort[i] + '$', 'i'); - } - return _monthsParse; - }(months, monthsShort)), - weekdays : "nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota".split("_"), - weekdaysShort : "ne_po_ut_st_št_pi_so".split("_"), - weekdaysMin : "ne_po_ut_st_št_pi_so".split("_"), + weekdays : 'nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota'.split('_'), + weekdaysShort : 'ne_po_ut_st_št_pi_so'.split('_'), + weekdaysMin : 'ne_po_ut_st_št_pi_so'.split('_'), longDateFormat : { - LT: "H:mm", - L : "DD.MM.YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY LT", - LLLL : "dddd D. MMMM YYYY LT" + LT: 'H:mm', + LTS : 'H:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY H:mm', + LLLL : 'dddd D. MMMM YYYY H:mm' }, calendar : { - sameDay: "[dnes o] LT", + sameDay: '[dnes o] LT', nextDay: '[zajtra o] LT', nextWeek: function () { switch (this.day()) { @@ -130,11 +120,11 @@ return '[minulú sobotu o] LT'; } }, - sameElse: "L" + sameElse: 'L' }, relativeTime : { - future : "za %s", - past : "pred %s", + future : 'za %s', + past : 'pred %s', s : translate, m : translate, mm : translate, @@ -147,10 +137,14 @@ y : translate, yy : translate }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return sk; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/sl.js b/lib/javascripts/moment_locale/sl.js index 6174ae63e2..68d261e271 100644 --- a/lib/javascripts/moment_locale/sl.js +++ b/lib/javascripts/moment_locale/sl.js @@ -1,89 +1,99 @@ -// moment.js locale configuration -// locale : slovenian (sl) -// author : Robert Sedovšek : https://github.com/sedovsek +//! moment.js locale configuration +//! locale : slovenian (sl) +//! author : Robert Sedovšek : https://github.com/sedovsek -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - function translate(number, withoutSuffix, key) { - var result = number + " "; +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + function processRelativeTime(number, withoutSuffix, key, isFuture) { + var result = number + ' '; switch (key) { + case 's': + return withoutSuffix || isFuture ? 'nekaj sekund' : 'nekaj sekundami'; case 'm': return withoutSuffix ? 'ena minuta' : 'eno minuto'; case 'mm': if (number === 1) { - result += 'minuta'; + result += withoutSuffix ? 'minuta' : 'minuto'; } else if (number === 2) { - result += 'minuti'; - } else if (number === 3 || number === 4) { - result += 'minute'; + result += withoutSuffix || isFuture ? 'minuti' : 'minutama'; + } else if (number < 5) { + result += withoutSuffix || isFuture ? 'minute' : 'minutami'; } else { - result += 'minut'; + result += withoutSuffix || isFuture ? 'minut' : 'minutami'; } return result; case 'h': return withoutSuffix ? 'ena ura' : 'eno uro'; case 'hh': if (number === 1) { - result += 'ura'; + result += withoutSuffix ? 'ura' : 'uro'; } else if (number === 2) { - result += 'uri'; - } else if (number === 3 || number === 4) { - result += 'ure'; + result += withoutSuffix || isFuture ? 'uri' : 'urama'; + } else if (number < 5) { + result += withoutSuffix || isFuture ? 'ure' : 'urami'; } else { - result += 'ur'; + result += withoutSuffix || isFuture ? 'ur' : 'urami'; } return result; + case 'd': + return withoutSuffix || isFuture ? 'en dan' : 'enim dnem'; case 'dd': if (number === 1) { - result += 'dan'; + result += withoutSuffix || isFuture ? 'dan' : 'dnem'; + } else if (number === 2) { + result += withoutSuffix || isFuture ? 'dni' : 'dnevoma'; } else { - result += 'dni'; + result += withoutSuffix || isFuture ? 'dni' : 'dnevi'; } return result; + case 'M': + return withoutSuffix || isFuture ? 'en mesec' : 'enim mesecem'; case 'MM': if (number === 1) { - result += 'mesec'; + result += withoutSuffix || isFuture ? 'mesec' : 'mesecem'; } else if (number === 2) { - result += 'meseca'; - } else if (number === 3 || number === 4) { - result += 'mesece'; + result += withoutSuffix || isFuture ? 'meseca' : 'mesecema'; + } else if (number < 5) { + result += withoutSuffix || isFuture ? 'mesece' : 'meseci'; } else { - result += 'mesecev'; + result += withoutSuffix || isFuture ? 'mesecev' : 'meseci'; } return result; + case 'y': + return withoutSuffix || isFuture ? 'eno leto' : 'enim letom'; case 'yy': if (number === 1) { - result += 'leto'; + result += withoutSuffix || isFuture ? 'leto' : 'letom'; } else if (number === 2) { - result += 'leti'; - } else if (number === 3 || number === 4) { - result += 'leta'; + result += withoutSuffix || isFuture ? 'leti' : 'letoma'; + } else if (number < 5) { + result += withoutSuffix || isFuture ? 'leta' : 'leti'; } else { - result += 'let'; + result += withoutSuffix || isFuture ? 'let' : 'leti'; } return result; } } - return moment.defineLocale('sl', { - months : "januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"), - monthsShort : "jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"), - weekdays : "nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota".split("_"), - weekdaysShort : "ned._pon._tor._sre._čet._pet._sob.".split("_"), - weekdaysMin : "ne_po_to_sr_če_pe_so".split("_"), + var sl = moment.defineLocale('sl', { + months : 'januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december'.split('_'), + monthsShort : 'jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.'.split('_'), + weekdays : 'nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota'.split('_'), + weekdaysShort : 'ned._pon._tor._sre._čet._pet._sob.'.split('_'), + weekdaysMin : 'ne_po_to_sr_če_pe_so'.split('_'), longDateFormat : { - LT : "H:mm", - L : "DD. MM. YYYY", - LL : "D. MMMM YYYY", - LLL : "D. MMMM YYYY LT", - LLLL : "dddd, D. MMMM YYYY LT" + LT : 'H:mm', + LTS : 'H:mm:ss', + L : 'DD. MM. YYYY', + LL : 'D. MMMM YYYY', + LLL : 'D. MMMM YYYY H:mm', + LLLL : 'dddd, D. MMMM YYYY H:mm' }, calendar : { sameDay : '[danes ob] LT', @@ -108,9 +118,11 @@ lastWeek : function () { switch (this.day()) { case 0: + return '[prejšnjo] [nedeljo] [ob] LT'; case 3: + return '[prejšnjo] [sredo] [ob] LT'; case 6: - return '[prejšnja] dddd [ob] LT'; + return '[prejšnjo] [soboto] [ob] LT'; case 1: case 2: case 4: @@ -121,24 +133,28 @@ sameElse : 'L' }, relativeTime : { - future : "čez %s", - past : "%s nazaj", - s : "nekaj sekund", - m : translate, - mm : translate, - h : translate, - hh : translate, - d : "en dan", - dd : translate, - M : "en mesec", - MM : translate, - y : "eno leto", - yy : translate + future : 'čez %s', + past : 'pred %s', + s : processRelativeTime, + m : processRelativeTime, + mm : processRelativeTime, + h : processRelativeTime, + hh : processRelativeTime, + d : processRelativeTime, + dd : processRelativeTime, + M : processRelativeTime, + MM : processRelativeTime, + y : processRelativeTime, + yy : processRelativeTime }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return sl; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/sq.js b/lib/javascripts/moment_locale/sq.js index 4a3dfea725..69dca2017c 100644 --- a/lib/javascripts/moment_locale/sq.js +++ b/lib/javascripts/moment_locale/sq.js @@ -1,33 +1,37 @@ -// moment.js locale configuration -// locale : Albanian (sq) -// author : Flakërim Ismani : https://github.com/flakerimi -// author: Menelion Elensúle: https://github.com/Oire (tests) -// author : Oerd Cukalla : https://github.com/oerd (fixes) +//! moment.js locale configuration +//! locale : Albanian (sq) +//! author : Flakërim Ismani : https://github.com/flakerimi +//! author: Menelion Elensúle: https://github.com/Oire (tests) +//! author : Oerd Cukalla : https://github.com/oerd (fixes) -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('sq', { - months : "Janar_Shkurt_Mars_Prill_Maj_Qershor_Korrik_Gusht_Shtator_Tetor_Nëntor_Dhjetor".split("_"), - monthsShort : "Jan_Shk_Mar_Pri_Maj_Qer_Kor_Gus_Sht_Tet_Nën_Dhj".split("_"), - weekdays : "E Diel_E Hënë_E Martë_E Mërkurë_E Enjte_E Premte_E Shtunë".split("_"), - weekdaysShort : "Die_Hën_Mar_Mër_Enj_Pre_Sht".split("_"), - weekdaysMin : "D_H_Ma_Më_E_P_Sh".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var sq = moment.defineLocale('sq', { + months : 'Janar_Shkurt_Mars_Prill_Maj_Qershor_Korrik_Gusht_Shtator_Tetor_Nëntor_Dhjetor'.split('_'), + monthsShort : 'Jan_Shk_Mar_Pri_Maj_Qer_Kor_Gus_Sht_Tet_Nën_Dhj'.split('_'), + weekdays : 'E Diel_E Hënë_E Martë_E Mërkurë_E Enjte_E Premte_E Shtunë'.split('_'), + weekdaysShort : 'Die_Hën_Mar_Mër_Enj_Pre_Sht'.split('_'), + weekdaysMin : 'D_H_Ma_Më_E_P_Sh'.split('_'), + meridiemParse: /PD|MD/, + isPM: function (input) { + return input.charAt(0) === 'M'; + }, meridiem : function (hours, minutes, isLower) { return hours < 12 ? 'PD' : 'MD'; }, longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' }, calendar : { sameDay : '[Sot në] LT', @@ -38,24 +42,28 @@ sameElse : 'L' }, relativeTime : { - future : "në %s", - past : "%s më parë", - s : "disa sekonda", - m : "një minutë", - mm : "%d minuta", - h : "një orë", - hh : "%d orë", - d : "një ditë", - dd : "%d ditë", - M : "një muaj", - MM : "%d muaj", - y : "një vit", - yy : "%d vite" + future : 'në %s', + past : '%s më parë', + s : 'disa sekonda', + m : 'një minutë', + mm : '%d minuta', + h : 'një orë', + hh : '%d orë', + d : 'një ditë', + dd : '%d ditë', + M : 'një muaj', + MM : '%d muaj', + y : 'një vit', + yy : '%d vite' }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return sq; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/sr-cyrl.js b/lib/javascripts/moment_locale/sr-cyrl.js index ef6e7ce4bb..c72cca7707 100644 --- a/lib/javascripts/moment_locale/sr-cyrl.js +++ b/lib/javascripts/moment_locale/sr-cyrl.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : Serbian-cyrillic (sr-cyrl) -// author : Milan Janačković : https://github.com/milan-j +//! moment.js locale configuration +//! locale : Serbian-cyrillic (sr-cyrl) +//! author : Milan Janačković : https://github.com/milan-j + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var translator = { words: { //Different grammatical cases m: ['један минут', 'једне минуте'], @@ -34,23 +33,23 @@ } }; - return moment.defineLocale('sr-cyrl', { + var sr_cyrl = moment.defineLocale('sr-cyrl', { months: ['јануар', 'фебруар', 'март', 'април', 'мај', 'јун', 'јул', 'август', 'септембар', 'октобар', 'новембар', 'децембар'], monthsShort: ['јан.', 'феб.', 'мар.', 'апр.', 'мај', 'јун', 'јул', 'авг.', 'сеп.', 'окт.', 'нов.', 'дец.'], weekdays: ['недеља', 'понедељак', 'уторак', 'среда', 'четвртак', 'петак', 'субота'], weekdaysShort: ['нед.', 'пон.', 'уто.', 'сре.', 'чет.', 'пет.', 'суб.'], weekdaysMin: ['не', 'по', 'ут', 'ср', 'че', 'пе', 'су'], longDateFormat: { - LT: "H:mm", - L: "DD. MM. YYYY", - LL: "D. MMMM YYYY", - LLL: "D. MMMM YYYY LT", - LLLL: "dddd, D. MMMM YYYY LT" + LT: 'H:mm', + LTS : 'H:mm:ss', + L: 'DD. MM. YYYY', + LL: 'D. MMMM YYYY', + LLL: 'D. MMMM YYYY H:mm', + LLLL: 'dddd, D. MMMM YYYY H:mm' }, calendar: { sameDay: '[данас у] LT', nextDay: '[сутра у] LT', - nextWeek: function () { switch (this.day()) { case 0: @@ -82,24 +81,28 @@ sameElse : 'L' }, relativeTime : { - future : "за %s", - past : "пре %s", - s : "неколико секунди", + future : 'за %s', + past : 'пре %s', + s : 'неколико секунди', m : translator.translate, mm : translator.translate, h : translator.translate, hh : translator.translate, - d : "дан", + d : 'дан', dd : translator.translate, - M : "месец", + M : 'месец', MM : translator.translate, - y : "годину", + y : 'годину', yy : translator.translate }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return sr_cyrl; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/sr.js b/lib/javascripts/moment_locale/sr.js index 86e8e84a6b..4dab6f48ce 100644 --- a/lib/javascripts/moment_locale/sr.js +++ b/lib/javascripts/moment_locale/sr.js @@ -1,16 +1,15 @@ -// moment.js locale configuration -// locale : Serbian-latin (sr) -// author : Milan Janačković : https://github.com/milan-j +//! moment.js locale configuration +//! locale : Serbian-latin (sr) +//! author : Milan Janačković : https://github.com/milan-j + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var translator = { words: { //Different grammatical cases m: ['jedan minut', 'jedne minute'], @@ -34,23 +33,23 @@ } }; - return moment.defineLocale('sr', { + var sr = moment.defineLocale('sr', { months: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'], monthsShort: ['jan.', 'feb.', 'mar.', 'apr.', 'maj', 'jun', 'jul', 'avg.', 'sep.', 'okt.', 'nov.', 'dec.'], weekdays: ['nedelja', 'ponedeljak', 'utorak', 'sreda', 'četvrtak', 'petak', 'subota'], weekdaysShort: ['ned.', 'pon.', 'uto.', 'sre.', 'čet.', 'pet.', 'sub.'], weekdaysMin: ['ne', 'po', 'ut', 'sr', 'če', 'pe', 'su'], longDateFormat: { - LT: "H:mm", - L: "DD. MM. YYYY", - LL: "D. MMMM YYYY", - LLL: "D. MMMM YYYY LT", - LLLL: "dddd, D. MMMM YYYY LT" + LT: 'H:mm', + LTS : 'H:mm:ss', + L: 'DD. MM. YYYY', + LL: 'D. MMMM YYYY', + LLL: 'D. MMMM YYYY H:mm', + LLLL: 'dddd, D. MMMM YYYY H:mm' }, calendar: { sameDay: '[danas u] LT', nextDay: '[sutra u] LT', - nextWeek: function () { switch (this.day()) { case 0: @@ -82,24 +81,28 @@ sameElse : 'L' }, relativeTime : { - future : "za %s", - past : "pre %s", - s : "nekoliko sekundi", + future : 'za %s', + past : 'pre %s', + s : 'nekoliko sekundi', m : translator.translate, mm : translator.translate, h : translator.translate, hh : translator.translate, - d : "dan", + d : 'dan', dd : translator.translate, - M : "mesec", + M : 'mesec', MM : translator.translate, - y : "godinu", + y : 'godinu', yy : translator.translate }, + ordinalParse: /\d{1,2}\./, ordinal : '%d.', week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return sr; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/sv.js b/lib/javascripts/moment_locale/sv.js index 9e39a30109..5af3bb72c4 100644 --- a/lib/javascripts/moment_locale/sv.js +++ b/lib/javascripts/moment_locale/sv.js @@ -1,52 +1,53 @@ -// moment.js locale configuration -// locale : swedish (sv) -// author : Jens Alm : https://github.com/ulmus +//! moment.js locale configuration +//! locale : swedish (sv) +//! author : Jens Alm : https://github.com/ulmus -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('sv', { - months : "januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"), - monthsShort : "jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"), - weekdays : "söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"), - weekdaysShort : "sön_mån_tis_ons_tor_fre_lör".split("_"), - weekdaysMin : "sö_må_ti_on_to_fr_lö".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var sv = moment.defineLocale('sv', { + months : 'januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december'.split('_'), + monthsShort : 'jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec'.split('_'), + weekdays : 'söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag'.split('_'), + weekdaysShort : 'sön_mån_tis_ons_tor_fre_lör'.split('_'), + weekdaysMin : 'sö_må_ti_on_to_fr_lö'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "YYYY-MM-DD", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'YYYY-MM-DD', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' }, calendar : { sameDay: '[Idag] LT', nextDay: '[Imorgon] LT', lastDay: '[Igår] LT', - nextWeek: 'dddd LT', - lastWeek: '[Förra] dddd[en] LT', + nextWeek: '[På] dddd LT', + lastWeek: '[I] dddd[s] LT', sameElse: 'L' }, relativeTime : { - future : "om %s", - past : "för %s sedan", - s : "några sekunder", - m : "en minut", - mm : "%d minuter", - h : "en timme", - hh : "%d timmar", - d : "en dag", - dd : "%d dagar", - M : "en månad", - MM : "%d månader", - y : "ett år", - yy : "%d år" + future : 'om %s', + past : 'för %s sedan', + s : 'några sekunder', + m : 'en minut', + mm : '%d minuter', + h : 'en timme', + hh : '%d timmar', + d : 'en dag', + dd : '%d dagar', + M : 'en månad', + MM : '%d månader', + y : 'ett år', + yy : '%d år' }, + ordinalParse: /\d{1,2}(e|a)/, ordinal : function (number) { var b = number % 10, output = (~~(number % 100 / 10) === 1) ? 'e' : @@ -60,4 +61,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return sv; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/sw.js b/lib/javascripts/moment_locale/sw.js new file mode 100644 index 0000000000..8017f7d473 --- /dev/null +++ b/lib/javascripts/moment_locale/sw.js @@ -0,0 +1,58 @@ +//! moment.js locale configuration +//! locale : swahili (sw) +//! author : Fahad Kassim : https://github.com/fadsel + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var sw = moment.defineLocale('sw', { + months : 'Januari_Februari_Machi_Aprili_Mei_Juni_Julai_Agosti_Septemba_Oktoba_Novemba_Desemba'.split('_'), + monthsShort : 'Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ago_Sep_Okt_Nov_Des'.split('_'), + weekdays : 'Jumapili_Jumatatu_Jumanne_Jumatano_Alhamisi_Ijumaa_Jumamosi'.split('_'), + weekdaysShort : 'Jpl_Jtat_Jnne_Jtan_Alh_Ijm_Jmos'.split('_'), + weekdaysMin : 'J2_J3_J4_J5_Al_Ij_J1'.split('_'), + longDateFormat : { + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' + }, + calendar : { + sameDay : '[leo saa] LT', + nextDay : '[kesho saa] LT', + nextWeek : '[wiki ijayo] dddd [saat] LT', + lastDay : '[jana] LT', + lastWeek : '[wiki iliyopita] dddd [saat] LT', + sameElse : 'L' + }, + relativeTime : { + future : '%s baadaye', + past : 'tokea %s', + s : 'hivi punde', + m : 'dakika moja', + mm : 'dakika %d', + h : 'saa limoja', + hh : 'masaa %d', + d : 'siku moja', + dd : 'masiku %d', + M : 'mwezi mmoja', + MM : 'miezi %d', + y : 'mwaka mmoja', + yy : 'miaka %d' + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 7 // The week that contains Jan 1st is the first week of the year. + } + }); + + return sw; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/ta.js b/lib/javascripts/moment_locale/ta.js index 963d40349f..2766509554 100644 --- a/lib/javascripts/moment_locale/ta.js +++ b/lib/javascripts/moment_locale/ta.js @@ -1,53 +1,52 @@ -// moment.js locale configuration -// locale : tamil (ta) -// author : Arjunkumar Krishnamoorthy : https://github.com/tk120404 +//! moment.js locale configuration +//! locale : tamil (ta) +//! author : Arjunkumar Krishnamoorthy : https://github.com/tk120404 -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - /*var symbolMap = { - '1': '௧', - '2': '௨', - '3': '௩', - '4': '௪', - '5': '௫', - '6': '௬', - '7': '௭', - '8': '௮', - '9': '௯', - '0': '௦' - }, - numberMap = { - '௧': '1', - '௨': '2', - '௩': '3', - '௪': '4', - '௫': '5', - '௬': '6', - '௭': '7', - '௮': '8', - '௯': '9', - '௦': '0' - }; */ +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; - return moment.defineLocale('ta', { - months : 'ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்'.split("_"), - monthsShort : 'ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்'.split("_"), - weekdays : 'ஞாயிற்றுக்கிழமை_திங்கட்கிழமை_செவ்வாய்கிழமை_புதன்கிழமை_வியாழக்கிழமை_வெள்ளிக்கிழமை_சனிக்கிழமை'.split("_"), - weekdaysShort : 'ஞாயிறு_திங்கள்_செவ்வாய்_புதன்_வியாழன்_வெள்ளி_சனி'.split("_"), - weekdaysMin : 'ஞா_தி_செ_பு_வி_வெ_ச'.split("_"), + + var symbolMap = { + '1': '௧', + '2': '௨', + '3': '௩', + '4': '௪', + '5': '௫', + '6': '௬', + '7': '௭', + '8': '௮', + '9': '௯', + '0': '௦' + }, numberMap = { + '௧': '1', + '௨': '2', + '௩': '3', + '௪': '4', + '௫': '5', + '௬': '6', + '௭': '7', + '௮': '8', + '௯': '9', + '௦': '0' + }; + + var ta = moment.defineLocale('ta', { + months : 'ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்'.split('_'), + monthsShort : 'ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்'.split('_'), + weekdays : 'ஞாயிற்றுக்கிழமை_திங்கட்கிழமை_செவ்வாய்கிழமை_புதன்கிழமை_வியாழக்கிழமை_வெள்ளிக்கிழமை_சனிக்கிழமை'.split('_'), + weekdaysShort : 'ஞாயிறு_திங்கள்_செவ்வாய்_புதன்_வியாழன்_வெள்ளி_சனி'.split('_'), + weekdaysMin : 'ஞா_தி_செ_பு_வி_வெ_ச'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY, LT", - LLLL : "dddd, D MMMM YYYY, LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY, HH:mm', + LLLL : 'dddd, D MMMM YYYY, HH:mm' }, calendar : { sameDay : '[இன்று] LT', @@ -58,21 +57,25 @@ sameElse : 'L' }, relativeTime : { - future : "%s இல்", - past : "%s முன்", - s : "ஒரு சில விநாடிகள்", - m : "ஒரு நிமிடம்", - mm : "%d நிமிடங்கள்", - h : "ஒரு மணி நேரம்", - hh : "%d மணி நேரம்", - d : "ஒரு நாள்", - dd : "%d நாட்கள்", - M : "ஒரு மாதம்", - MM : "%d மாதங்கள்", - y : "ஒரு வருடம்", - yy : "%d ஆண்டுகள்" + future : '%s இல்', + past : '%s முன்', + s : 'ஒரு சில விநாடிகள்', + m : 'ஒரு நிமிடம்', + mm : '%d நிமிடங்கள்', + h : 'ஒரு மணி நேரம்', + hh : '%d மணி நேரம்', + d : 'ஒரு நாள்', + dd : '%d நாட்கள்', + M : 'ஒரு மாதம்', + MM : '%d மாதங்கள்', + y : 'ஒரு வருடம்', + yy : '%d ஆண்டுகள்' }, -/* preparse: function (string) { + ordinalParse: /\d{1,2}வது/, + ordinal : function (number) { + return number + 'வது'; + }, + preparse: function (string) { return string.replace(/[௧௨௩௪௫௬௭௮௯௦]/g, function (match) { return numberMap[match]; }); @@ -81,27 +84,38 @@ return string.replace(/\d/g, function (match) { return symbolMap[match]; }); - },*/ - ordinal : function (number) { - return number + 'வது'; }, - - // refer http://ta.wikipedia.org/s/1er1 - + meridiemParse: /யாமம்|வைகறை|காலை|நண்பகல்|எற்பாடு|மாலை/, meridiem : function (hour, minute, isLower) { - if (hour >= 6 && hour <= 10) { - return " காலை"; - } else if (hour >= 10 && hour <= 14) { - return " நண்பகல்"; - } else if (hour >= 14 && hour <= 18) { - return " எற்பாடு"; - } else if (hour >= 18 && hour <= 20) { - return " மாலை"; - } else if (hour >= 20 && hour <= 24) { - return " இரவு"; - } else if (hour >= 0 && hour <= 6) { - return " வைகறை"; + if (hour < 2) { + return ' யாமம்'; + } else if (hour < 6) { + return ' வைகறை'; // வைகறை + } else if (hour < 10) { + return ' காலை'; // காலை + } else if (hour < 14) { + return ' நண்பகல்'; // நண்பகல் + } else if (hour < 18) { + return ' எற்பாடு'; // எற்பாடு + } else if (hour < 22) { + return ' மாலை'; // மாலை + } else { + return ' யாமம்'; + } + }, + meridiemHour : function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === 'யாமம்') { + return hour < 2 ? hour : hour + 12; + } else if (meridiem === 'வைகறை' || meridiem === 'காலை') { + return hour; + } else if (meridiem === 'நண்பகல்') { + return hour >= 10 ? hour : hour + 12; + } else { + return hour + 12; } }, week : { @@ -109,4 +123,7 @@ doy : 6 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return ta; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/te.js b/lib/javascripts/moment_locale/te.js new file mode 100644 index 0000000000..775651df17 --- /dev/null +++ b/lib/javascripts/moment_locale/te.js @@ -0,0 +1,88 @@ +//! moment.js locale configuration +//! locale : telugu (te) +//! author : Krishna Chaitanya Thota : https://github.com/kcthota + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var te = moment.defineLocale('te', { + months : 'జనవరి_ఫిబ్రవరి_మార్చి_ఏప్రిల్_మే_జూన్_జూలై_ఆగస్టు_సెప్టెంబర్_అక్టోబర్_నవంబర్_డిసెంబర్'.split('_'), + monthsShort : 'జన._ఫిబ్ర._మార్చి_ఏప్రి._మే_జూన్_జూలై_ఆగ._సెప్._అక్టో._నవ._డిసె.'.split('_'), + weekdays : 'ఆదివారం_సోమవారం_మంగళవారం_బుధవారం_గురువారం_శుక్రవారం_శనివారం'.split('_'), + weekdaysShort : 'ఆది_సోమ_మంగళ_బుధ_గురు_శుక్ర_శని'.split('_'), + weekdaysMin : 'ఆ_సో_మం_బు_గు_శు_శ'.split('_'), + longDateFormat : { + LT : 'A h:mm', + LTS : 'A h:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY, A h:mm', + LLLL : 'dddd, D MMMM YYYY, A h:mm' + }, + calendar : { + sameDay : '[నేడు] LT', + nextDay : '[రేపు] LT', + nextWeek : 'dddd, LT', + lastDay : '[నిన్న] LT', + lastWeek : '[గత] dddd, LT', + sameElse : 'L' + }, + relativeTime : { + future : '%s లో', + past : '%s క్రితం', + s : 'కొన్ని క్షణాలు', + m : 'ఒక నిమిషం', + mm : '%d నిమిషాలు', + h : 'ఒక గంట', + hh : '%d గంటలు', + d : 'ఒక రోజు', + dd : '%d రోజులు', + M : 'ఒక నెల', + MM : '%d నెలలు', + y : 'ఒక సంవత్సరం', + yy : '%d సంవత్సరాలు' + }, + ordinalParse : /\d{1,2}వ/, + ordinal : '%dవ', + meridiemParse: /రాత్రి|ఉదయం|మధ్యాహ్నం|సాయంత్రం/, + meridiemHour : function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === 'రాత్రి') { + return hour < 4 ? hour : hour + 12; + } else if (meridiem === 'ఉదయం') { + return hour; + } else if (meridiem === 'మధ్యాహ్నం') { + return hour >= 10 ? hour : hour + 12; + } else if (meridiem === 'సాయంత్రం') { + return hour + 12; + } + }, + meridiem : function (hour, minute, isLower) { + if (hour < 4) { + return 'రాత్రి'; + } else if (hour < 10) { + return 'ఉదయం'; + } else if (hour < 17) { + return 'మధ్యాహ్నం'; + } else if (hour < 20) { + return 'సాయంత్రం'; + } else { + return 'రాత్రి'; + } + }, + week : { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + } + }); + + return te; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/th.js b/lib/javascripts/moment_locale/th.js index 30b41e69aa..ac325429df 100644 --- a/lib/javascripts/moment_locale/th.js +++ b/lib/javascripts/moment_locale/th.js @@ -1,34 +1,38 @@ -// moment.js locale configuration -// locale : thai (th) -// author : Kridsada Thanabulpong : https://github.com/sirn +//! moment.js locale configuration +//! locale : thai (th) +//! author : Kridsada Thanabulpong : https://github.com/sirn -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('th', { - months : "มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"), - monthsShort : "มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"), - weekdays : "อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"), - weekdaysShort : "อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"), // yes, three characters difference - weekdaysMin : "อา._จ._อ._พ._พฤ._ศ._ส.".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var th = moment.defineLocale('th', { + months : 'มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม'.split('_'), + monthsShort : 'มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา'.split('_'), + weekdays : 'อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์'.split('_'), + weekdaysShort : 'อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์'.split('_'), // yes, three characters difference + weekdaysMin : 'อา._จ._อ._พ._พฤ._ศ._ส.'.split('_'), longDateFormat : { - LT : "H นาฬิกา m นาที", - L : "YYYY/MM/DD", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY เวลา LT", - LLLL : "วันddddที่ D MMMM YYYY เวลา LT" + LT : 'H นาฬิกา m นาที', + LTS : 'H นาฬิกา m นาที s วินาที', + L : 'YYYY/MM/DD', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY เวลา H นาฬิกา m นาที', + LLLL : 'วันddddที่ D MMMM YYYY เวลา H นาฬิกา m นาที' + }, + meridiemParse: /ก่อนเที่ยง|หลังเที่ยง/, + isPM: function (input) { + return input === 'หลังเที่ยง'; }, meridiem : function (hour, minute, isLower) { if (hour < 12) { - return "ก่อนเที่ยง"; + return 'ก่อนเที่ยง'; } else { - return "หลังเที่ยง"; + return 'หลังเที่ยง'; } }, calendar : { @@ -40,19 +44,22 @@ sameElse : 'L' }, relativeTime : { - future : "อีก %s", - past : "%sที่แล้ว", - s : "ไม่กี่วินาที", - m : "1 นาที", - mm : "%d นาที", - h : "1 ชั่วโมง", - hh : "%d ชั่วโมง", - d : "1 วัน", - dd : "%d วัน", - M : "1 เดือน", - MM : "%d เดือน", - y : "1 ปี", - yy : "%d ปี" + future : 'อีก %s', + past : '%sที่แล้ว', + s : 'ไม่กี่วินาที', + m : '1 นาที', + mm : '%d นาที', + h : '1 ชั่วโมง', + hh : '%d ชั่วโมง', + d : '1 วัน', + dd : '%d วัน', + M : '1 เดือน', + MM : '%d เดือน', + y : '1 ปี', + yy : '%d ปี' } }); -})); + + return th; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/tl-ph.js b/lib/javascripts/moment_locale/tl-ph.js index dfacf18df3..d101fd9256 100644 --- a/lib/javascripts/moment_locale/tl-ph.js +++ b/lib/javascripts/moment_locale/tl-ph.js @@ -1,31 +1,31 @@ -// moment.js locale configuration -// locale : Tagalog/Filipino (tl-ph) -// author : Dan Hagman +//! moment.js locale configuration +//! locale : Tagalog/Filipino (tl-ph) +//! author : Dan Hagman -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('tl-ph', { - months : "Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre".split("_"), - monthsShort : "Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis".split("_"), - weekdays : "Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado".split("_"), - weekdaysShort : "Lin_Lun_Mar_Miy_Huw_Biy_Sab".split("_"), - weekdaysMin : "Li_Lu_Ma_Mi_Hu_Bi_Sab".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var tl_ph = moment.defineLocale('tl-ph', { + months : 'Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre'.split('_'), + monthsShort : 'Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis'.split('_'), + weekdays : 'Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado'.split('_'), + weekdaysShort : 'Lin_Lun_Mar_Miy_Huw_Biy_Sab'.split('_'), + weekdaysMin : 'Li_Lu_Ma_Mi_Hu_Bi_Sab'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "MM/D/YYYY", - LL : "MMMM D, YYYY", - LLL : "MMMM D, YYYY LT", - LLLL : "dddd, MMMM DD, YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'MM/D/YYYY', + LL : 'MMMM D, YYYY', + LLL : 'MMMM D, YYYY HH:mm', + LLLL : 'dddd, MMMM DD, YYYY HH:mm' }, calendar : { - sameDay: "[Ngayon sa] LT", + sameDay: '[Ngayon sa] LT', nextDay: '[Bukas sa] LT', nextWeek: 'dddd [sa] LT', lastDay: '[Kahapon sa] LT', @@ -33,20 +33,21 @@ sameElse: 'L' }, relativeTime : { - future : "sa loob ng %s", - past : "%s ang nakalipas", - s : "ilang segundo", - m : "isang minuto", - mm : "%d minuto", - h : "isang oras", - hh : "%d oras", - d : "isang araw", - dd : "%d araw", - M : "isang buwan", - MM : "%d buwan", - y : "isang taon", - yy : "%d taon" + future : 'sa loob ng %s', + past : '%s ang nakalipas', + s : 'ilang segundo', + m : 'isang minuto', + mm : '%d minuto', + h : 'isang oras', + hh : '%d oras', + d : 'isang araw', + dd : '%d araw', + M : 'isang buwan', + MM : '%d buwan', + y : 'isang taon', + yy : '%d taon' }, + ordinalParse: /\d{1,2}/, ordinal : function (number) { return number; }, @@ -55,4 +56,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return tl_ph; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/tlh.js b/lib/javascripts/moment_locale/tlh.js new file mode 100644 index 0000000000..4ae53ef719 --- /dev/null +++ b/lib/javascripts/moment_locale/tlh.js @@ -0,0 +1,119 @@ +//! moment.js locale configuration +//! locale : Klingon (tlh) +//! author : Dominika Kruk : https://github.com/amaranthrose + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var numbersNouns = 'pagh_wa’_cha’_wej_loS_vagh_jav_Soch_chorgh_Hut'.split('_'); + + function translateFuture(output) { + var time = output; + time = (output.indexOf('jaj') !== -1) ? + time.slice(0, -3) + 'leS' : + (output.indexOf('jar') !== -1) ? + time.slice(0, -3) + 'waQ' : + (output.indexOf('DIS') !== -1) ? + time.slice(0, -3) + 'nem' : + time + ' pIq'; + return time; + } + + function translatePast(output) { + var time = output; + time = (output.indexOf('jaj') !== -1) ? + time.slice(0, -3) + 'Hu’' : + (output.indexOf('jar') !== -1) ? + time.slice(0, -3) + 'wen' : + (output.indexOf('DIS') !== -1) ? + time.slice(0, -3) + 'ben' : + time + ' ret'; + return time; + } + + function translate(number, withoutSuffix, string, isFuture) { + var numberNoun = numberAsNoun(number); + switch (string) { + case 'mm': + return numberNoun + ' tup'; + case 'hh': + return numberNoun + ' rep'; + case 'dd': + return numberNoun + ' jaj'; + case 'MM': + return numberNoun + ' jar'; + case 'yy': + return numberNoun + ' DIS'; + } + } + + function numberAsNoun(number) { + var hundred = Math.floor((number % 1000) / 100), + ten = Math.floor((number % 100) / 10), + one = number % 10, + word = ''; + if (hundred > 0) { + word += numbersNouns[hundred] + 'vatlh'; + } + if (ten > 0) { + word += ((word !== '') ? ' ' : '') + numbersNouns[ten] + 'maH'; + } + if (one > 0) { + word += ((word !== '') ? ' ' : '') + numbersNouns[one]; + } + return (word === '') ? 'pagh' : word; + } + + var tlh = moment.defineLocale('tlh', { + months : 'tera’ jar wa’_tera’ jar cha’_tera’ jar wej_tera’ jar loS_tera’ jar vagh_tera’ jar jav_tera’ jar Soch_tera’ jar chorgh_tera’ jar Hut_tera’ jar wa’maH_tera’ jar wa’maH wa’_tera’ jar wa’maH cha’'.split('_'), + monthsShort : 'jar wa’_jar cha’_jar wej_jar loS_jar vagh_jar jav_jar Soch_jar chorgh_jar Hut_jar wa’maH_jar wa’maH wa’_jar wa’maH cha’'.split('_'), + weekdays : 'lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj'.split('_'), + weekdaysShort : 'lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj'.split('_'), + weekdaysMin : 'lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj'.split('_'), + longDateFormat : { + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' + }, + calendar : { + sameDay: '[DaHjaj] LT', + nextDay: '[wa’leS] LT', + nextWeek: 'LLL', + lastDay: '[wa’Hu’] LT', + lastWeek: 'LLL', + sameElse: 'L' + }, + relativeTime : { + future : translateFuture, + past : translatePast, + s : 'puS lup', + m : 'wa’ tup', + mm : translate, + h : 'wa’ rep', + hh : translate, + d : 'wa’ jaj', + dd : translate, + M : 'wa’ jar', + MM : translate, + y : 'wa’ DIS', + yy : translate + }, + ordinalParse: /\d{1,2}\./, + ordinal : '%d.', + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } + }); + + return tlh; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/tr.js b/lib/javascripts/moment_locale/tr.js index e6c2adab7c..638edbb025 100644 --- a/lib/javascripts/moment_locale/tr.js +++ b/lib/javascripts/moment_locale/tr.js @@ -1,55 +1,50 @@ -// moment.js locale configuration -// locale : turkish (tr) -// authors : Erhan Gundogan : https://github.com/erhangundogan, -// Burak Yiğit Kaya: https://github.com/BYK +//! moment.js locale configuration +//! locale : turkish (tr) +//! authors : Erhan Gundogan : https://github.com/erhangundogan, +//! Burak Yiğit Kaya: https://github.com/BYK + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { var suffixes = { - 1: "'inci", - 5: "'inci", - 8: "'inci", - 70: "'inci", - 80: "'inci", - - 2: "'nci", - 7: "'nci", - 20: "'nci", - 50: "'nci", - - 3: "'üncü", - 4: "'üncü", - 100: "'üncü", - - 6: "'ncı", - - 9: "'uncu", - 10: "'uncu", - 30: "'uncu", - - 60: "'ıncı", - 90: "'ıncı" + 1: '\'inci', + 5: '\'inci', + 8: '\'inci', + 70: '\'inci', + 80: '\'inci', + 2: '\'nci', + 7: '\'nci', + 20: '\'nci', + 50: '\'nci', + 3: '\'üncü', + 4: '\'üncü', + 100: '\'üncü', + 6: '\'ncı', + 9: '\'uncu', + 10: '\'uncu', + 30: '\'uncu', + 60: '\'ıncı', + 90: '\'ıncı' }; - return moment.defineLocale('tr', { - months : "Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"), - monthsShort : "Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"), - weekdays : "Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"), - weekdaysShort : "Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"), - weekdaysMin : "Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"), + var tr = moment.defineLocale('tr', { + months : 'Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık'.split('_'), + monthsShort : 'Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara'.split('_'), + weekdays : 'Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi'.split('_'), + weekdaysShort : 'Paz_Pts_Sal_Çar_Per_Cum_Cts'.split('_'), + weekdaysMin : 'Pz_Pt_Sa_Ça_Pe_Cu_Ct'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD.MM.YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd, D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd, D MMMM YYYY HH:mm' }, calendar : { sameDay : '[bugün saat] LT', @@ -60,28 +55,28 @@ sameElse : 'L' }, relativeTime : { - future : "%s sonra", - past : "%s önce", - s : "birkaç saniye", - m : "bir dakika", - mm : "%d dakika", - h : "bir saat", - hh : "%d saat", - d : "bir gün", - dd : "%d gün", - M : "bir ay", - MM : "%d ay", - y : "bir yıl", - yy : "%d yıl" + future : '%s sonra', + past : '%s önce', + s : 'birkaç saniye', + m : 'bir dakika', + mm : '%d dakika', + h : 'bir saat', + hh : '%d saat', + d : 'bir gün', + dd : '%d gün', + M : 'bir ay', + MM : '%d ay', + y : 'bir yıl', + yy : '%d yıl' }, + ordinalParse: /\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/, ordinal : function (number) { if (number === 0) { // special case for zero - return number + "'ıncı"; + return number + '\'ıncı'; } var a = number % 10, b = number % 100 - a, c = number >= 100 ? 100 : null; - return number + (suffixes[a] || suffixes[b] || suffixes[c]); }, week : { @@ -89,4 +84,7 @@ doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return tr; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/tzl.js b/lib/javascripts/moment_locale/tzl.js new file mode 100644 index 0000000000..498f7c040e --- /dev/null +++ b/lib/javascripts/moment_locale/tzl.js @@ -0,0 +1,87 @@ +//! moment.js locale configuration +//! locale : talossan (tzl) +//! author : Robin van der Vliet : https://github.com/robin0van0der0v with the help of Iustì Canun + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + + // After the year there should be a slash and the amount of years since December 26, 1979 in Roman numerals. + // This is currently too difficult (maybe even impossible) to add. + var tzl = moment.defineLocale('tzl', { + months : 'Januar_Fevraglh_Març_Avrïu_Mai_Gün_Julia_Guscht_Setemvar_Listopäts_Noemvar_Zecemvar'.split('_'), + monthsShort : 'Jan_Fev_Mar_Avr_Mai_Gün_Jul_Gus_Set_Lis_Noe_Zec'.split('_'), + weekdays : 'Súladi_Lúneçi_Maitzi_Márcuri_Xhúadi_Viénerçi_Sáturi'.split('_'), + weekdaysShort : 'Súl_Lún_Mai_Már_Xhú_Vié_Sát'.split('_'), + weekdaysMin : 'Sú_Lú_Ma_Má_Xh_Vi_Sá'.split('_'), + longDateFormat : { + LT : 'HH.mm', + LTS : 'HH.mm.ss', + L : 'DD.MM.YYYY', + LL : 'D. MMMM [dallas] YYYY', + LLL : 'D. MMMM [dallas] YYYY HH.mm', + LLLL : 'dddd, [li] D. MMMM [dallas] YYYY HH.mm' + }, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'd\'o' : 'D\'O'; + } else { + return isLower ? 'd\'a' : 'D\'A'; + } + }, + calendar : { + sameDay : '[oxhi à] LT', + nextDay : '[demà à] LT', + nextWeek : 'dddd [à] LT', + lastDay : '[ieiri à] LT', + lastWeek : '[sür el] dddd [lasteu à] LT', + sameElse : 'L' + }, + relativeTime : { + future : 'osprei %s', + past : 'ja%s', + s : processRelativeTime, + m : processRelativeTime, + mm : processRelativeTime, + h : processRelativeTime, + hh : processRelativeTime, + d : processRelativeTime, + dd : processRelativeTime, + M : processRelativeTime, + MM : processRelativeTime, + y : processRelativeTime, + yy : processRelativeTime + }, + ordinalParse: /\d{1,2}\./, + ordinal : '%d.', + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } + }); + + function processRelativeTime(number, withoutSuffix, key, isFuture) { + var format = { + 's': ['viensas secunds', '\'iensas secunds'], + 'm': ['\'n míut', '\'iens míut'], + 'mm': [number + ' míuts', '' + number + ' míuts'], + 'h': ['\'n þora', '\'iensa þora'], + 'hh': [number + ' þoras', '' + number + ' þoras'], + 'd': ['\'n ziua', '\'iensa ziua'], + 'dd': [number + ' ziuas', '' + number + ' ziuas'], + 'M': ['\'n mes', '\'iens mes'], + 'MM': [number + ' mesen', '' + number + ' mesen'], + 'y': ['\'n ar', '\'iens ar'], + 'yy': [number + ' ars', '' + number + ' ars'] + }; + return isFuture ? format[key][0] : (withoutSuffix ? format[key][0] : format[key][1]); + } + + return tzl; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/tzm-latn.js b/lib/javascripts/moment_locale/tzm-latn.js index 1411e161b3..712f5f5e41 100644 --- a/lib/javascripts/moment_locale/tzm-latn.js +++ b/lib/javascripts/moment_locale/tzm-latn.js @@ -1,31 +1,31 @@ -// moment.js locale configuration -// locale : Morocco Central Atlas Tamaziɣt in Latin (tzm-latn) -// author : Abdel Said : https://github.com/abdelsaid +//! moment.js locale configuration +//! locale : Morocco Central Atlas Tamaziɣt in Latin (tzm-latn) +//! author : Abdel Said : https://github.com/abdelsaid -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('tzm-latn', { - months : "innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir".split("_"), - monthsShort : "innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir".split("_"), - weekdays : "asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas".split("_"), - weekdaysShort : "asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas".split("_"), - weekdaysMin : "asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var tzm_latn = moment.defineLocale('tzm-latn', { + months : 'innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir'.split('_'), + monthsShort : 'innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir'.split('_'), + weekdays : 'asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas'.split('_'), + weekdaysShort : 'asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas'.split('_'), + weekdaysMin : 'asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' }, calendar : { - sameDay: "[asdkh g] LT", + sameDay: '[asdkh g] LT', nextDay: '[aska g] LT', nextWeek: 'dddd [g] LT', lastDay: '[assant g] LT', @@ -33,23 +33,26 @@ sameElse: 'L' }, relativeTime : { - future : "dadkh s yan %s", - past : "yan %s", - s : "imik", - m : "minuḍ", - mm : "%d minuḍ", - h : "saɛa", - hh : "%d tassaɛin", - d : "ass", - dd : "%d ossan", - M : "ayowr", - MM : "%d iyyirn", - y : "asgas", - yy : "%d isgasn" + future : 'dadkh s yan %s', + past : 'yan %s', + s : 'imik', + m : 'minuḍ', + mm : '%d minuḍ', + h : 'saɛa', + hh : '%d tassaɛin', + d : 'ass', + dd : '%d ossan', + M : 'ayowr', + MM : '%d iyyirn', + y : 'asgas', + yy : '%d isgasn' }, week : { dow : 6, // Saturday is the first day of the week. doy : 12 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return tzm_latn; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/tzm.js b/lib/javascripts/moment_locale/tzm.js index 615eb979cd..6b8acc08ee 100644 --- a/lib/javascripts/moment_locale/tzm.js +++ b/lib/javascripts/moment_locale/tzm.js @@ -1,31 +1,31 @@ -// moment.js locale configuration -// locale : Morocco Central Atlas Tamaziɣt (tzm) -// author : Abdel Said : https://github.com/abdelsaid +//! moment.js locale configuration +//! locale : Morocco Central Atlas Tamaziɣt (tzm) +//! author : Abdel Said : https://github.com/abdelsaid -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('tzm', { - months : "ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ".split("_"), - monthsShort : "ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ".split("_"), - weekdays : "ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ".split("_"), - weekdaysShort : "ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ".split("_"), - weekdaysMin : "ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var tzm = moment.defineLocale('tzm', { + months : 'ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ'.split('_'), + monthsShort : 'ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ'.split('_'), + weekdays : 'ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ'.split('_'), + weekdaysShort : 'ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ'.split('_'), + weekdaysMin : 'ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "dddd D MMMM YYYY LT" + LT : 'HH:mm', + LTS: 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'dddd D MMMM YYYY HH:mm' }, calendar : { - sameDay: "[ⴰⵙⴷⵅ ⴴ] LT", + sameDay: '[ⴰⵙⴷⵅ ⴴ] LT', nextDay: '[ⴰⵙⴽⴰ ⴴ] LT', nextWeek: 'dddd [ⴴ] LT', lastDay: '[ⴰⵚⴰⵏⵜ ⴴ] LT', @@ -33,23 +33,26 @@ sameElse: 'L' }, relativeTime : { - future : "ⴷⴰⴷⵅ ⵙ ⵢⴰⵏ %s", - past : "ⵢⴰⵏ %s", - s : "ⵉⵎⵉⴽ", - m : "ⵎⵉⵏⵓⴺ", - mm : "%d ⵎⵉⵏⵓⴺ", - h : "ⵙⴰⵄⴰ", - hh : "%d ⵜⴰⵙⵙⴰⵄⵉⵏ", - d : "ⴰⵙⵙ", - dd : "%d oⵙⵙⴰⵏ", - M : "ⴰⵢoⵓⵔ", - MM : "%d ⵉⵢⵢⵉⵔⵏ", - y : "ⴰⵙⴳⴰⵙ", - yy : "%d ⵉⵙⴳⴰⵙⵏ" + future : 'ⴷⴰⴷⵅ ⵙ ⵢⴰⵏ %s', + past : 'ⵢⴰⵏ %s', + s : 'ⵉⵎⵉⴽ', + m : 'ⵎⵉⵏⵓⴺ', + mm : '%d ⵎⵉⵏⵓⴺ', + h : 'ⵙⴰⵄⴰ', + hh : '%d ⵜⴰⵙⵙⴰⵄⵉⵏ', + d : 'ⴰⵙⵙ', + dd : '%d oⵙⵙⴰⵏ', + M : 'ⴰⵢoⵓⵔ', + MM : '%d ⵉⵢⵢⵉⵔⵏ', + y : 'ⴰⵙⴳⴰⵙ', + yy : '%d ⵉⵙⴳⴰⵙⵏ' }, week : { dow : 6, // Saturday is the first day of the week. doy : 12 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return tzm; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/uk.js b/lib/javascripts/moment_locale/uk.js index f27d9f3e31..80c34973c5 100644 --- a/lib/javascripts/moment_locale/uk.js +++ b/lib/javascripts/moment_locale/uk.js @@ -1,26 +1,24 @@ -// moment.js locale configuration -// locale : ukrainian (uk) -// author : zemlanin : https://github.com/zemlanin -// Author : Menelion Elensúle : https://github.com/Oire +//! moment.js locale configuration +//! locale : ukrainian (uk) +//! author : zemlanin : https://github.com/zemlanin +//! Author : Menelion Elensúle : https://github.com/Oire + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { function plural(word, num) { var forms = word.split('_'); return num % 10 === 1 && num % 100 !== 11 ? forms[0] : (num % 10 >= 2 && num % 10 <= 4 && (num % 100 < 10 || num % 100 >= 20) ? forms[1] : forms[2]); } - function relativeTimeWithPlural(number, withoutSuffix, key) { var format = { - 'mm': 'хвилина_хвилини_хвилин', - 'hh': 'година_години_годин', + 'mm': withoutSuffix ? 'хвилина_хвилини_хвилин' : 'хвилину_хвилини_хвилин', + 'hh': withoutSuffix ? 'година_години_годин' : 'годину_години_годин', 'dd': 'день_дні_днів', 'MM': 'місяць_місяці_місяців', 'yy': 'рік_роки_років' @@ -35,54 +33,41 @@ return number + ' ' + plural(format[key], +number); } } - - function monthsCaseReplace(m, format) { - var months = { - 'nominative': 'січень_лютий_березень_квітень_травень_червень_липень_серпень_вересень_жовтень_листопад_грудень'.split('_'), - 'accusative': 'січня_лютого_березня_квітня_травня_червня_липня_серпня_вересня_жовтня_листопада_грудня'.split('_') - }, - - nounCase = (/D[oD]? *MMMM?/).test(format) ? - 'accusative' : - 'nominative'; - - return months[nounCase][m.month()]; - } - function weekdaysCaseReplace(m, format) { var weekdays = { 'nominative': 'неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота'.split('_'), 'accusative': 'неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу'.split('_'), 'genitive': 'неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи'.split('_') }, - nounCase = (/(\[[ВвУу]\]) ?dddd/).test(format) ? 'accusative' : ((/\[?(?:минулої|наступної)? ?\] ?dddd/).test(format) ? 'genitive' : 'nominative'); - return weekdays[nounCase][m.day()]; } - function processHoursFunction(str) { return function () { return str + 'о' + (this.hours() === 11 ? 'б' : '') + '] LT'; }; } - return moment.defineLocale('uk', { - months : monthsCaseReplace, - monthsShort : "січ_лют_бер_квіт_трав_черв_лип_серп_вер_жовт_лист_груд".split("_"), + var uk = moment.defineLocale('uk', { + months : { + 'format': 'січня_лютого_березня_квітня_травня_червня_липня_серпня_вересня_жовтня_листопада_грудня'.split('_'), + 'standalone': 'січень_лютий_березень_квітень_травень_червень_липень_серпень_вересень_жовтень_листопад_грудень'.split('_') + }, + monthsShort : 'січ_лют_бер_квіт_трав_черв_лип_серп_вер_жовт_лист_груд'.split('_'), weekdays : weekdaysCaseReplace, - weekdaysShort : "нд_пн_вт_ср_чт_пт_сб".split("_"), - weekdaysMin : "нд_пн_вт_ср_чт_пт_сб".split("_"), + weekdaysShort : 'нд_пн_вт_ср_чт_пт_сб'.split('_'), + weekdaysMin : 'нд_пн_вт_ср_чт_пт_сб'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD.MM.YYYY", - LL : "D MMMM YYYY р.", - LLL : "D MMMM YYYY р., LT", - LLLL : "dddd, D MMMM YYYY р., LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD.MM.YYYY', + LL : 'D MMMM YYYY р.', + LLL : 'D MMMM YYYY р., HH:mm', + LLLL : 'dddd, D MMMM YYYY р., HH:mm' }, calendar : { sameDay: processHoursFunction('[Сьогодні '), @@ -105,35 +90,37 @@ sameElse: 'L' }, relativeTime : { - future : "за %s", - past : "%s тому", - s : "декілька секунд", + future : 'за %s', + past : '%s тому', + s : 'декілька секунд', m : relativeTimeWithPlural, mm : relativeTimeWithPlural, - h : "годину", + h : 'годину', hh : relativeTimeWithPlural, - d : "день", + d : 'день', dd : relativeTimeWithPlural, - M : "місяць", + M : 'місяць', MM : relativeTimeWithPlural, - y : "рік", + y : 'рік', yy : relativeTimeWithPlural }, - // M. E.: those two are virtually unused but a user might want to implement them for his/her website for some reason - + meridiemParse: /ночі|ранку|дня|вечора/, + isPM: function (input) { + return /^(дня|вечора)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { - return "ночі"; + return 'ночі'; } else if (hour < 12) { - return "ранку"; + return 'ранку'; } else if (hour < 17) { - return "дня"; + return 'дня'; } else { - return "вечора"; + return 'вечора'; } }, - + ordinalParse: /\d{1,2}-(й|го)/, ordinal: function (number, period) { switch (period) { case 'M': @@ -148,10 +135,12 @@ return number; } }, - week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 1st is the first week of the year. } }); -})); + + return uk; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/uz.js b/lib/javascripts/moment_locale/uz.js index 7cf541c527..fcf594e6a7 100644 --- a/lib/javascripts/moment_locale/uz.js +++ b/lib/javascripts/moment_locale/uz.js @@ -1,28 +1,28 @@ -// moment.js locale configuration -// locale : uzbek (uz) -// author : Sardor Muminov : https://github.com/muminoff +//! moment.js locale configuration +//! locale : uzbek (uz) +//! author : Sardor Muminov : https://github.com/muminoff -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('uz', { - months : "январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"), - monthsShort : "янв_фев_мар_апр_май_июн_июл_авг_сен_окт_ноя_дек".split("_"), - weekdays : "Якшанба_Душанба_Сешанба_Чоршанба_Пайшанба_Жума_Шанба".split("_"), - weekdaysShort : "Якш_Душ_Сеш_Чор_Пай_Жум_Шан".split("_"), - weekdaysMin : "Як_Ду_Се_Чо_Па_Жу_Ша".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var uz = moment.defineLocale('uz', { + months : 'январ_феврал_март_апрел_май_июн_июл_август_сентябр_октябр_ноябр_декабр'.split('_'), + monthsShort : 'янв_фев_мар_апр_май_июн_июл_авг_сен_окт_ноя_дек'.split('_'), + weekdays : 'Якшанба_Душанба_Сешанба_Чоршанба_Пайшанба_Жума_Шанба'.split('_'), + weekdaysShort : 'Якш_Душ_Сеш_Чор_Пай_Жум_Шан'.split('_'), + weekdaysMin : 'Як_Ду_Се_Чо_Па_Жу_Ша'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM YYYY", - LLL : "D MMMM YYYY LT", - LLLL : "D MMMM YYYY, dddd LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM YYYY', + LLL : 'D MMMM YYYY HH:mm', + LLLL : 'D MMMM YYYY, dddd HH:mm' }, calendar : { sameDay : '[Бугун соат] LT [да]', @@ -33,23 +33,26 @@ sameElse : 'L' }, relativeTime : { - future : "Якин %s ичида", - past : "Бир неча %s олдин", - s : "фурсат", - m : "бир дакика", - mm : "%d дакика", - h : "бир соат", - hh : "%d соат", - d : "бир кун", - dd : "%d кун", - M : "бир ой", - MM : "%d ой", - y : "бир йил", - yy : "%d йил" + future : 'Якин %s ичида', + past : 'Бир неча %s олдин', + s : 'фурсат', + m : 'бир дакика', + mm : '%d дакика', + h : 'бир соат', + hh : '%d соат', + d : 'бир кун', + dd : '%d кун', + M : 'бир ой', + MM : '%d ой', + y : 'бир йил', + yy : '%d йил' }, week : { dow : 1, // Monday is the first day of the week. doy : 7 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return uz; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/vi.js b/lib/javascripts/moment_locale/vi.js index 3f8f5f5afc..0e7ba8c353 100644 --- a/lib/javascripts/moment_locale/vi.js +++ b/lib/javascripts/moment_locale/vi.js @@ -1,35 +1,35 @@ -// moment.js locale configuration -// locale : vietnamese (vi) -// author : Bang Nguyen : https://github.com/bangnk +//! moment.js locale configuration +//! locale : vietnamese (vi) +//! author : Bang Nguyen : https://github.com/bangnk -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('vi', { - months : "tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12".split("_"), - monthsShort : "Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12".split("_"), - weekdays : "chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy".split("_"), - weekdaysShort : "CN_T2_T3_T4_T5_T6_T7".split("_"), - weekdaysMin : "CN_T2_T3_T4_T5_T6_T7".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var vi = moment.defineLocale('vi', { + months : 'tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12'.split('_'), + monthsShort : 'Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12'.split('_'), + weekdays : 'chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy'.split('_'), + weekdaysShort : 'CN_T2_T3_T4_T5_T6_T7'.split('_'), + weekdaysMin : 'CN_T2_T3_T4_T5_T6_T7'.split('_'), longDateFormat : { - LT : "HH:mm", - L : "DD/MM/YYYY", - LL : "D MMMM [năm] YYYY", - LLL : "D MMMM [năm] YYYY LT", - LLLL : "dddd, D MMMM [năm] YYYY LT", - l : "DD/M/YYYY", - ll : "D MMM YYYY", - lll : "D MMM YYYY LT", - llll : "ddd, D MMM YYYY LT" + LT : 'HH:mm', + LTS : 'HH:mm:ss', + L : 'DD/MM/YYYY', + LL : 'D MMMM [năm] YYYY', + LLL : 'D MMMM [năm] YYYY HH:mm', + LLLL : 'dddd, D MMMM [năm] YYYY HH:mm', + l : 'DD/M/YYYY', + ll : 'D MMM YYYY', + lll : 'D MMM YYYY HH:mm', + llll : 'ddd, D MMM YYYY HH:mm' }, calendar : { - sameDay: "[Hôm nay lúc] LT", + sameDay: '[Hôm nay lúc] LT', nextDay: '[Ngày mai lúc] LT', nextWeek: 'dddd [tuần tới lúc] LT', lastDay: '[Hôm qua lúc] LT', @@ -37,20 +37,21 @@ sameElse: 'L' }, relativeTime : { - future : "%s tới", - past : "%s trước", - s : "vài giây", - m : "một phút", - mm : "%d phút", - h : "một giờ", - hh : "%d giờ", - d : "một ngày", - dd : "%d ngày", - M : "một tháng", - MM : "%d tháng", - y : "một năm", - yy : "%d năm" + future : '%s tới', + past : '%s trước', + s : 'vài giây', + m : 'một phút', + mm : '%d phút', + h : 'một giờ', + hh : '%d giờ', + d : 'một ngày', + dd : '%d ngày', + M : 'một tháng', + MM : '%d tháng', + y : 'một năm', + yy : '%d năm' }, + ordinalParse: /\d{1,2}/, ordinal : function (number) { return number; }, @@ -59,4 +60,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return vi; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/zh-cn.js b/lib/javascripts/moment_locale/zh-cn.js index 78ea965b8f..aec5ede49e 100644 --- a/lib/javascripts/moment_locale/zh-cn.js +++ b/lib/javascripts/moment_locale/zh-cn.js @@ -1,103 +1,119 @@ -// moment.js locale configuration -// locale : chinese (zh-cn) -// author : suupic : https://github.com/suupic -// author : Zeno Zeng : https://github.com/zenozeng +//! moment.js locale configuration +//! locale : chinese (zh-cn) +//! author : suupic : https://github.com/suupic +//! author : Zeno Zeng : https://github.com/zenozeng -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('zh-cn', { - months : "一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"), - monthsShort : "1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"), - weekdays : "星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"), - weekdaysShort : "周日_周一_周二_周三_周四_周五_周六".split("_"), - weekdaysMin : "日_一_二_三_四_五_六".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var zh_cn = moment.defineLocale('zh-cn', { + months : '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split('_'), + monthsShort : '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'), + weekdays : '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'), + weekdaysShort : '周日_周一_周二_周三_周四_周五_周六'.split('_'), + weekdaysMin : '日_一_二_三_四_五_六'.split('_'), longDateFormat : { - LT : "Ah点mm", - L : "YYYY-MM-DD", - LL : "YYYY年MMMD日", - LLL : "YYYY年MMMD日LT", - LLLL : "YYYY年MMMD日ddddLT", - l : "YYYY-MM-DD", - ll : "YYYY年MMMD日", - lll : "YYYY年MMMD日LT", - llll : "YYYY年MMMD日ddddLT" + LT : 'Ah点mm分', + LTS : 'Ah点m分s秒', + L : 'YYYY-MM-DD', + LL : 'YYYY年MMMD日', + LLL : 'YYYY年MMMD日Ah点mm分', + LLLL : 'YYYY年MMMD日ddddAh点mm分', + l : 'YYYY-MM-DD', + ll : 'YYYY年MMMD日', + lll : 'YYYY年MMMD日Ah点mm分', + llll : 'YYYY年MMMD日ddddAh点mm分' + }, + meridiemParse: /凌晨|早上|上午|中午|下午|晚上/, + meridiemHour: function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === '凌晨' || meridiem === '早上' || + meridiem === '上午') { + return hour; + } else if (meridiem === '下午' || meridiem === '晚上') { + return hour + 12; + } else { + // '中午' + return hour >= 11 ? hour : hour + 12; + } }, meridiem : function (hour, minute, isLower) { var hm = hour * 100 + minute; if (hm < 600) { - return "凌晨"; + return '凌晨'; } else if (hm < 900) { - return "早上"; + return '早上'; } else if (hm < 1130) { - return "上午"; + return '上午'; } else if (hm < 1230) { - return "中午"; + return '中午'; } else if (hm < 1800) { - return "下午"; + return '下午'; } else { - return "晚上"; + return '晚上'; } }, calendar : { sameDay : function () { - return this.minutes() === 0 ? "[今天]Ah[点整]" : "[今天]LT"; + return this.minutes() === 0 ? '[今天]Ah[点整]' : '[今天]LT'; }, nextDay : function () { - return this.minutes() === 0 ? "[明天]Ah[点整]" : "[明天]LT"; + return this.minutes() === 0 ? '[明天]Ah[点整]' : '[明天]LT'; }, lastDay : function () { - return this.minutes() === 0 ? "[昨天]Ah[点整]" : "[昨天]LT"; + return this.minutes() === 0 ? '[昨天]Ah[点整]' : '[昨天]LT'; }, nextWeek : function () { var startOfWeek, prefix; startOfWeek = moment().startOf('week'); prefix = this.unix() - startOfWeek.unix() >= 7 * 24 * 3600 ? '[下]' : '[本]'; - return this.minutes() === 0 ? prefix + "dddAh点整" : prefix + "dddAh点mm"; + return this.minutes() === 0 ? prefix + 'dddAh点整' : prefix + 'dddAh点mm'; }, lastWeek : function () { var startOfWeek, prefix; startOfWeek = moment().startOf('week'); prefix = this.unix() < startOfWeek.unix() ? '[上]' : '[本]'; - return this.minutes() === 0 ? prefix + "dddAh点整" : prefix + "dddAh点mm"; + return this.minutes() === 0 ? prefix + 'dddAh点整' : prefix + 'dddAh点mm'; }, sameElse : 'LL' }, + ordinalParse: /\d{1,2}(日|月|周)/, ordinal : function (number, period) { switch (period) { - case "d": - case "D": - case "DDD": - return number + "日"; - case "M": - return number + "月"; - case "w": - case "W": - return number + "周"; + case 'd': + case 'D': + case 'DDD': + return number + '日'; + case 'M': + return number + '月'; + case 'w': + case 'W': + return number + '周'; default: return number; } }, relativeTime : { - future : "%s内", - past : "%s前", - s : "几秒", - m : "1分钟", - mm : "%d分钟", - h : "1小时", - hh : "%d小时", - d : "1天", - dd : "%d天", - M : "1个月", - MM : "%d个月", - y : "1年", - yy : "%d年" + future : '%s内', + past : '%s前', + s : '几秒', + m : '1 分钟', + mm : '%d 分钟', + h : '1 小时', + hh : '%d 小时', + d : '1 天', + dd : '%d 天', + M : '1 个月', + MM : '%d 个月', + y : '1 年', + yy : '%d 年' }, week : { // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 @@ -105,4 +121,7 @@ doy : 4 // The week that contains Jan 4th is the first week of the year. } }); -})); + + return zh_cn; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/zh-tw.js b/lib/javascripts/moment_locale/zh-tw.js index edb1fb9879..bf3a333c41 100644 --- a/lib/javascripts/moment_locale/zh-tw.js +++ b/lib/javascripts/moment_locale/zh-tw.js @@ -1,45 +1,58 @@ -// moment.js locale configuration -// locale : traditional chinese (zh-tw) -// author : Ben : https://github.com/ben-lin +//! moment.js locale configuration +//! locale : traditional chinese (zh-tw) +//! author : Ben : https://github.com/ben-lin -(function (factory) { - if (typeof define === 'function' && define.amd) { - define(['moment'], factory); // AMD - } else if (typeof exports === 'object') { - module.exports = factory(require('../moment')); // Node - } else { - factory(window.moment); // Browser global - } -}(function (moment) { - return moment.defineLocale('zh-tw', { - months : "一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"), - monthsShort : "1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"), - weekdays : "星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"), - weekdaysShort : "週日_週一_週二_週三_週四_週五_週六".split("_"), - weekdaysMin : "日_一_二_三_四_五_六".split("_"), +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + && typeof require === 'function' ? factory(require('../moment')) : + typeof define === 'function' && define.amd ? define(['moment'], factory) : + factory(global.moment) +}(this, function (moment) { 'use strict'; + + + var zh_tw = moment.defineLocale('zh-tw', { + months : '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split('_'), + monthsShort : '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'), + weekdays : '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'), + weekdaysShort : '週日_週一_週二_週三_週四_週五_週六'.split('_'), + weekdaysMin : '日_一_二_三_四_五_六'.split('_'), longDateFormat : { - LT : "Ah點mm", - L : "YYYY年MMMD日", - LL : "YYYY年MMMD日", - LLL : "YYYY年MMMD日LT", - LLLL : "YYYY年MMMD日ddddLT", - l : "YYYY年MMMD日", - ll : "YYYY年MMMD日", - lll : "YYYY年MMMD日LT", - llll : "YYYY年MMMD日ddddLT" + LT : 'Ah點mm分', + LTS : 'Ah點m分s秒', + L : 'YYYY年MMMD日', + LL : 'YYYY年MMMD日', + LLL : 'YYYY年MMMD日Ah點mm分', + LLLL : 'YYYY年MMMD日ddddAh點mm分', + l : 'YYYY年MMMD日', + ll : 'YYYY年MMMD日', + lll : 'YYYY年MMMD日Ah點mm分', + llll : 'YYYY年MMMD日ddddAh點mm分' + }, + meridiemParse: /早上|上午|中午|下午|晚上/, + meridiemHour : function (hour, meridiem) { + if (hour === 12) { + hour = 0; + } + if (meridiem === '早上' || meridiem === '上午') { + return hour; + } else if (meridiem === '中午') { + return hour >= 11 ? hour : hour + 12; + } else if (meridiem === '下午' || meridiem === '晚上') { + return hour + 12; + } }, meridiem : function (hour, minute, isLower) { var hm = hour * 100 + minute; if (hm < 900) { - return "早上"; + return '早上'; } else if (hm < 1130) { - return "上午"; + return '上午'; } else if (hm < 1230) { - return "中午"; + return '中午'; } else if (hm < 1800) { - return "下午"; + return '下午'; } else { - return "晚上"; + return '晚上'; } }, calendar : { @@ -50,35 +63,39 @@ lastWeek : '[上]ddddLT', sameElse : 'L' }, + ordinalParse: /\d{1,2}(日|月|週)/, ordinal : function (number, period) { switch (period) { - case "d" : - case "D" : - case "DDD" : - return number + "日"; - case "M" : - return number + "月"; - case "w" : - case "W" : - return number + "週"; + case 'd' : + case 'D' : + case 'DDD' : + return number + '日'; + case 'M' : + return number + '月'; + case 'w' : + case 'W' : + return number + '週'; default : return number; } }, relativeTime : { - future : "%s內", - past : "%s前", - s : "幾秒", - m : "一分鐘", - mm : "%d分鐘", - h : "一小時", - hh : "%d小時", - d : "一天", - dd : "%d天", - M : "一個月", - MM : "%d個月", - y : "一年", - yy : "%d年" + future : '%s內', + past : '%s前', + s : '幾秒', + m : '一分鐘', + mm : '%d分鐘', + h : '一小時', + hh : '%d小時', + d : '一天', + dd : '%d天', + M : '一個月', + MM : '%d個月', + y : '一年', + yy : '%d年' } }); -})); + + return zh_tw; + +})); \ No newline at end of file diff --git a/lib/javascripts/moment_locale/zh_CN.js b/lib/javascripts/moment_locale/zh_CN.js deleted file mode 100644 index 11fc0342ef..0000000000 --- a/lib/javascripts/moment_locale/zh_CN.js +++ /dev/null @@ -1,73 +0,0 @@ -// moment.js language configuration -// language : chinese -// author : suupic : https://github.com/suupic - -moment.lang('zh-cn', { - months : "一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"), - monthsShort : "1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"), - weekdays : "星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"), - weekdaysShort : "周日_周一_周二_周三_周四_周五_周六".split("_"), - weekdaysMin : "日_一_二_三_四_五_六".split("_"), - longDateFormat : { - LT : "Ah点mm", - L : "YYYY年MMMD日", - LL : "YYYY年MMMD日", - LLL : "YYYY年MMMD日LT", - LLLL : "YYYY年MMMD日ddddLT", - l : "YYYY年MMMD日", - ll : "YYYY年MMMD日", - lll : "YYYY年MMMD日LT", - llll : "YYYY年MMMD日ddddLT" - }, - meridiem : function (hour, minute, isLower) { - if (hour < 9) { - return "早上"; - } else if (hour < 11 && minute < 30) { - return "上午"; - } else if (hour < 13 && minute < 30) { - return "中午"; - } else if (hour < 18) { - return "下午"; - } else { - return "晚上"; - } - }, - calendar : { - sameDay : '[今天]LT', - nextDay : '[明天]LT', - nextWeek : '[下]ddddLT', - lastDay : '[昨天]LT', - lastWeek : '[上]ddddLT', - sameElse : 'L' - }, - ordinal : function (number, period) { - switch (period) { - case "d" : - case "D" : - case "DDD" : - return number + "日"; - case "M" : - return number + "月"; - case "w" : - case "W" : - return number + "周"; - default : - return number; - } - }, - relativeTime : { - future : "%s内", - past : "%s前", - s : "几秒", - m : "1分钟", - mm : "%d分钟", - h : "1小时", - hh : "%d小时", - d : "1天", - dd : "%d天", - M : "1个月", - MM : "%d个月", - y : "1年", - yy : "%d年" - } -}); diff --git a/lib/javascripts/moment_locale/zh_TW.js b/lib/javascripts/moment_locale/zh_TW.js deleted file mode 100644 index 8ac45323ae..0000000000 --- a/lib/javascripts/moment_locale/zh_TW.js +++ /dev/null @@ -1,73 +0,0 @@ -// moment.js language configuration -// language : traditional chinese (zh-tw) -// author : Ben : https://github.com/ben-lin - -moment.lang('zh-tw', { - months : "一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"), - monthsShort : "1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"), - weekdays : "星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"), - weekdaysShort : "週日_週一_週二_週三_週四_週五_週六".split("_"), - weekdaysMin : "日_一_二_三_四_五_六".split("_"), - longDateFormat : { - LT : "Ah點mm", - L : "YYYY年MMMD日", - LL : "YYYY年MMMD日", - LLL : "YYYY年MMMD日LT", - LLLL : "YYYY年MMMD日ddddLT", - l : "YYYY年MMMD日", - ll : "YYYY年MMMD日", - lll : "YYYY年MMMD日LT", - llll : "YYYY年MMMD日ddddLT" - }, - meridiem : function (hour, minute, isLower) { - if (hour < 9) { - return "早上"; - } else if (hour < 11 && minute < 30) { - return "上午"; - } else if (hour < 13 && minute < 30) { - return "中午"; - } else if (hour < 18) { - return "下午"; - } else { - return "晚上"; - } - }, - calendar : { - sameDay : '[今天]LT', - nextDay : '[明天]LT', - nextWeek : '[下]ddddLT', - lastDay : '[昨天]LT', - lastWeek : '[上]ddddLT', - sameElse : 'L' - }, - ordinal : function (number, period) { - switch (period) { - case "d" : - case "D" : - case "DDD" : - return number + "日"; - case "M" : - return number + "月"; - case "w" : - case "W" : - return number + "週"; - default : - return number; - } - }, - relativeTime : { - future : "%s內", - past : "%s前", - s : "幾秒", - m : "一分鐘", - mm : "%d分鐘", - h : "一小時", - hh : "%d小時", - d : "一天", - dd : "%d天", - M : "一個月", - MM : "%d個月", - y : "一年", - yy : "%d年" - } -}); From 89add4a4a2d52f8864d735f33a75a7bdb635ab01 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 5 Feb 2016 21:49:03 +0100 Subject: [PATCH 024/140] JsLocaleHelper should search for moment.js locale files moment.js uses a different naming conventions for locale files. E.g. "zh-zn" instead of "zh_ZN" and "nb" instead of "nb_NO" This change allows us to use the locale files without renaming which makes future upgrades of moment.js a lot easier. --- lib/js_locale_helper.rb | 9 +++++++++ spec/components/js_locale_helper_spec.rb | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index af06c33409..1374235eb4 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -123,7 +123,16 @@ module JsLocaleHelper end def self.moment_locale(locale_str) + # moment.js uses a different naming scheme for locale files + locale_str = locale_str.tr('_', '-').downcase filename = Rails.root + "lib/javascripts/moment_locale/#{locale_str}.js" + + unless File.exists?(filename) + # try the language without the territory + locale_str = locale_str.partition('-').first + filename = Rails.root + "lib/javascripts/moment_locale/#{locale_str}.js" + end + if File.exists?(filename) File.read(filename) << "\n" end || "" diff --git a/spec/components/js_locale_helper_spec.rb b/spec/components/js_locale_helper_spec.rb index ae0077f8be..337553f7fa 100644 --- a/spec/components/js_locale_helper_spec.rb +++ b/spec/components/js_locale_helper_spec.rb @@ -173,6 +173,16 @@ describe JsLocaleHelper do ctx.load(Rails.root + 'app/assets/javascripts/locales/i18n.js') ctx.eval(js) end + + it "finds moment.js locale file for #{locale[:value]}" do + content = JsLocaleHelper.moment_locale(locale[:value]) + + if (locale[:value] == 'en') + expect(content).to eq('') + else + expect(content).to_not eq('') + end + end end end From f7eb7f25bdb8fd57d6ebd912f27356bb144857b3 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 5 Feb 2016 21:49:21 +0100 Subject: [PATCH 025/140] UX: Use i18n for date picker --- .../javascripts/discourse/components/date-picker.js.es6 | 8 ++++++++ config/locales/client.en.yml | 2 ++ 2 files changed, 10 insertions(+) diff --git a/app/assets/javascripts/discourse/components/date-picker.js.es6 b/app/assets/javascripts/discourse/components/date-picker.js.es6 index d1c8e4fcef..621b0d1fd8 100644 --- a/app/assets/javascripts/discourse/components/date-picker.js.es6 +++ b/app/assets/javascripts/discourse/components/date-picker.js.es6 @@ -17,6 +17,14 @@ export default Em.Component.extend({ format: "YYYY-MM-DD", defaultDate: moment().add(1, "day").toDate(), minDate: new Date(), + firstDay: moment.localeData().firstDayOfWeek(), + i18n: { + previousMonth: I18n.t('dates.previous_month'), + nextMonth: I18n.t('dates.next_month'), + months: moment.months(), + weekdays: moment.weekdays(), + weekdaysShort: moment.weekdaysShort() + }, onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD")) }; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d98e1eb65e..73541fbbc7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -109,6 +109,8 @@ en: x_years: one: "1 year later" other: "%{count} years later" + previous_month: 'Previous Month' + next_month: 'Next Month' share: topic: 'share a link to this topic' post: 'post #%{postNumber}' From 8a7868be077d4e4d142f94d6e588c766d58a604a Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sat, 6 Feb 2016 02:19:48 +0530 Subject: [PATCH 026/140] FIX: validate user website --- app/models/user_profile.rb | 1 + app/services/user_updater.rb | 1 + spec/models/user_profile_spec.rb | 12 ++++++++++++ 3 files changed, 14 insertions(+) diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index 14aed1333d..32c0e2047c 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -2,6 +2,7 @@ class UserProfile < ActiveRecord::Base belongs_to :user, inverse_of: :user_profile validates :bio_raw, length: { maximum: 3000 } + validates :website, format: { with: /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,9}(([0-9]{1,5})?\/.*)?$)/ix }, allow_blank: true validates :user, presence: true before_save :cook after_save :trigger_badges diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 9ce7f57197..c6f236d2b3 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -104,6 +104,7 @@ class UserUpdater attr_reader :user, :guardian def format_url(website) + return nil if website.blank? website =~ /^http/ ? website : "http://#{website}" end end diff --git a/spec/models/user_profile_spec.rb b/spec/models/user_profile_spec.rb index 66720ec790..cbacd41769 100644 --- a/spec/models/user_profile_spec.rb +++ b/spec/models/user_profile_spec.rb @@ -37,6 +37,18 @@ describe UserProfile do expect(user_profile).not_to be_valid end + it "doesn't support invalid website" do + user_profile = Fabricate.build(:user_profile, website: "http://https://google.com") + user_profile.user = Fabricate.build(:user) + expect(user_profile).not_to be_valid + end + + it "supports valid website" do + user_profile = Fabricate.build(:user_profile, website: "https://google.com") + user_profile.user = Fabricate.build(:user) + expect(user_profile.valid?).to be true + end + describe 'after save' do let(:user) { Fabricate(:user) } From 8e135e460b8018359fba6f9a9193d549478e076f Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 5 Feb 2016 23:19:23 +0100 Subject: [PATCH 027/140] UX: Date Picker in user preferences was ugly --- app/assets/stylesheets/desktop/user.scss | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index eb11d0af8c..55a6268691 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -197,9 +197,9 @@ margin-bottom: 10px; } - table { + .user-invite-list { width: 100%; - margin-top: 10px; + margin-top: 15px; th { text-align: left; @@ -222,10 +222,6 @@ } } - .user-invite-list { - margin-top: 15px; - } - .user-invite-controls { background-color: dark-light-diff($primary, $secondary, 90%, -75%); padding: 5px 10px 0px 0; From 0270da555530f6210e9c41a1361945a0a0a5c957 Mon Sep 17 00:00:00 2001 From: cpradio Date: Sat, 6 Feb 2016 09:18:13 -0500 Subject: [PATCH 028/140] Enable block quotes to be readable on the Profile About Me section --- app/assets/stylesheets/desktop/user.scss | 5 +++++ app/assets/stylesheets/mobile/user.scss | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 55a6268691..9dbdb5e439 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -302,6 +302,11 @@ background: dark-light-choose(rgba($primary, .85), rgba($secondary, .85)); transition: margin .15s linear; + blockquote { + background-color: dark-light-diff($secondary, $primary, 30%, -70%); + border-left-color: dark-light-diff($secondary, $primary, 50%, -50%); + } + h1 { font-size: 2.143em; font-weight: normal; diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 7398fc6b5f..1b7d8c9f35 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -252,6 +252,11 @@ background-color: dark-light-choose(rgba($primary, .9), rgba($secondary, .9)); opacity: 0.8; + blockquote { + background-color: dark-light-diff($secondary, $primary, 30%, -70%); + border-left-color: dark-light-diff($secondary, $primary, 50%, -50%); + } + h1 { font-size: 2.143em; font-weight: normal; From 9dba5315677d4e46c50e6a349bac12f087b8d301 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Sat, 6 Feb 2016 22:30:24 +0100 Subject: [PATCH 029/140] FIX: Remove invalid 'http://' website from profiles --- db/migrate/20160206210202_remove_invalid_websites.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 db/migrate/20160206210202_remove_invalid_websites.rb diff --git a/db/migrate/20160206210202_remove_invalid_websites.rb b/db/migrate/20160206210202_remove_invalid_websites.rb new file mode 100644 index 0000000000..d0ae516ef8 --- /dev/null +++ b/db/migrate/20160206210202_remove_invalid_websites.rb @@ -0,0 +1,5 @@ +class RemoveInvalidWebsites < ActiveRecord::Migration + def change + execute "UPDATE user_profiles SET website = NULL WHERE website = 'http://'" + end +end From d81728f70d3d934d1aaab6d86072b91c6167ad06 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sat, 6 Feb 2016 14:22:46 -0800 Subject: [PATCH 030/140] don't allow relative dates to wrap --- app/assets/stylesheets/common/base/discourse.scss | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 95ccddffbc..8b8ab8040d 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -219,3 +219,17 @@ body { display: inline-block; } } + +// don't wrap relative dates, we want +// +// Jul 26, '15 +// +// not +// +// Jul +// 26, +// '15 +// +span.relative-date { + white-space:nowrap; +} From b0567f9c62b830a6be4cfcf2ec5aa519f8b256cb Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sun, 7 Feb 2016 23:39:07 +1100 Subject: [PATCH 031/140] FEATURE: automatically sync "move to inbox" / "archive" state on messages --- .../javascripts/discourse/controllers/topic.js.es6 | 8 ++++++++ app/controllers/topics_controller.rb | 12 ++++++------ app/models/group_archived_message.rb | 12 ++++++++++++ app/models/user_archived_message.rb | 11 +++++++++++ lib/post_creator.rb | 9 +++++++-- lib/topics_bulk_action.rb | 8 ++++---- 6 files changed, 48 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index e5f5d19d33..2fe9e6ee7b 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -632,6 +632,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { } return; } + case "move_to_inbox": { + topic.set("message_archived",false); + return; + } + case "archived": { + topic.set("message_archived",true); + return; + } default: { Em.Logger.warn("unknown topic bus message type", data); } diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index bc53a2e955..d30f1008ca 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -289,20 +289,20 @@ class TopicsController < ApplicationController allowed_groups = topic.allowed_groups .where('topic_allowed_groups.group_id IN (?)', group_ids).pluck(:id) allowed_groups.each do |id| - GroupArchivedMessage.where(group_id: id, topic_id: topic.id).destroy_all - if archive + GroupArchivedMessage.archive!(id, topic.id) group_id = id - GroupArchivedMessage.create!(group_id: id, topic_id: topic.id) + else + GroupArchivedMessage.move_to_inbox!(id, topic.id) end end end if topic.allowed_users.include?(current_user) - UserArchivedMessage.where(user_id: current_user.id, topic_id: topic.id).destroy_all - if archive - UserArchivedMessage.create!(user_id: current_user.id, topic_id: topic.id) + UserArchivedMessage.archive!(current_user.id, topic.id) + else + UserArchivedMessage.move_to_inbox!(current_user.id, topic.id) end end diff --git a/app/models/group_archived_message.rb b/app/models/group_archived_message.rb index b32092cb39..22e0fb9f19 100644 --- a/app/models/group_archived_message.rb +++ b/app/models/group_archived_message.rb @@ -1,6 +1,18 @@ class GroupArchivedMessage < ActiveRecord::Base belongs_to :user belongs_to :topic + + def self.move_to_inbox!(group_id, topic_id) + GroupArchivedMessage.where(group_id: group_id, topic_id: topic_id).destroy_all + MessageBus.publish("/topic/#{topic_id}", {type: "move_to_inbox"}, group_ids: [group_id]) + end + + def self.archive!(group_id, topic_id) + GroupArchivedMessage.where(group_id: group_id, topic_id: topic_id).destroy_all + GroupArchivedMessage.create!(group_id: group_id, topic_id: topic_id) + MessageBus.publish("/topic/#{topic_id}", {type: "archived"}, group_ids: [group_id]) + end + end # == Schema Information diff --git a/app/models/user_archived_message.rb b/app/models/user_archived_message.rb index 3f7c94e474..3a54b6ea6d 100644 --- a/app/models/user_archived_message.rb +++ b/app/models/user_archived_message.rb @@ -1,6 +1,17 @@ class UserArchivedMessage < ActiveRecord::Base belongs_to :user belongs_to :topic + + def self.move_to_inbox!(user_id, topic_id) + UserArchivedMessage.where(user_id: user_id, topic_id: topic_id).destroy_all + MessageBus.publish("/topic/#{topic_id}", {type: "move_to_inbox"}, user_ids: [user_id]) + end + + def self.archive!(user_id, topic_id) + UserArchivedMessage.where(user_id: user_id, topic_id: topic_id).destroy_all + UserArchivedMessage.create!(user_id: user_id, topic_id: topic_id) + MessageBus.publish("/topic/#{topic_id}", {type: "archived"}, user_ids: [user_id]) + end end # == Schema Information diff --git a/lib/post_creator.rb b/lib/post_creator.rb index f7151442b2..62bc2a5793 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -276,8 +276,13 @@ class PostCreator def unarchive_message return unless @topic.private_message? && @topic.id - UserArchivedMessage.where(topic_id: @topic.id).destroy_all - GroupArchivedMessage.where(topic_id: @topic.id).destroy_all + UserArchivedMessage.where(topic_id: @topic.id).pluck(:user_id).each do |user_id| + UserArchivedMessage.move_to_inbox!(user_id, @topic.id) + end + + GroupArchivedMessage.where(topic_id: @topic.id).pluck(:group_id).each do |group_id| + GroupArchivedMessage.move_to_inbox!(group_id, @topic.id) + end end private diff --git a/lib/topics_bulk_action.rb b/lib/topics_bulk_action.rb index 0fcdf8b713..541d283a1a 100644 --- a/lib/topics_bulk_action.rb +++ b/lib/topics_bulk_action.rb @@ -43,9 +43,9 @@ class TopicsBulkAction topics.each do |t| if guardian.can_see?(t) && t.private_message? if group - GroupArchivedMessage.where(group_id: group.id, topic_id: t.id).destroy_all + GroupArchivedMessage.move_to_inbox!(group.id, t.id) else - UserArchivedMessage.where(user_id: @user.id, topic_id: t.id).destroy_all + UserArchivedMessage.move_to_inbox!(@user.id,t.id) end end end @@ -56,9 +56,9 @@ class TopicsBulkAction topics.each do |t| if guardian.can_see?(t) && t.private_message? if group - GroupArchivedMessage.create!(group_id: group.id, topic_id: t.id) + GroupArchivedMessage.archive!(group.id, t.id) else - UserArchivedMessage.create!(user_id: @user.id, topic_id: t.id) + UserArchivedMessage.archive!(@user.id, t.id) end end end From b032c63773a09f82c1b1053d0dd4e29842d243d5 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 8 Feb 2016 08:44:12 +1100 Subject: [PATCH 032/140] FIX: properly defer authentication complete --- app/views/common/_discourse_javascript.html.erb | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index 8135819246..5f52c531ef 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -33,6 +33,18 @@ <%= script 'browser-update' %> From 38983bc977ae4140e49997d3cf5174e54c7014f4 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 8 Feb 2016 09:53:47 +1100 Subject: [PATCH 033/140] oops --- app/views/common/_discourse_javascript.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index 5f52c531ef..921ece2c5c 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -38,7 +38,7 @@ actions: { didTransition: function() { Em.run.next(function(){ - Discourse.authenticationComplete(<%=flash[:authentication_date].html_safe%>); + Discourse.authenticationComplete(<%=flash[:authentication_data].html_safe%>); }); return this._super(); } From b9e87320185a104c697cfb27835c0782d6a455da Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 8 Feb 2016 10:51:59 +1100 Subject: [PATCH 034/140] UX: tweak autocomplete to limit hijacking - Stop eating up back arrow when you hit @ - Clicking anywhere closes autocomplete - Forward arrow no longer issues autocompletion, instead functions as right arrow --- .../discourse/lib/autocomplete.js.es6 | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index aeb2ffb1db..71dce2d1d6 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -47,7 +47,8 @@ export default function(options) { $(this).off('keypress.autocomplete') .off('keydown.autocomplete') - .off('paste.autocomplete'); + .off('paste.autocomplete') + .off('click.autocomplete'); return; } @@ -276,6 +277,10 @@ export default function(options) { closeAutocomplete(); }); + $(this).on('click.autocomplete', function() { + closeAutocomplete(); + }); + $(this).on('paste.autocomplete', function() { _.delay(function(){ me.trigger("keydown"); @@ -375,16 +380,21 @@ export default function(options) { if (completeStart !== null) { caretPosition = Discourse.Utilities.caretPosition(me[0]); + // allow people to right arrow out of completion + if (e.which === keys.rightArrow && me[0].value[caretPosition] === ' ') { + closeAutocomplete(); + return true; + } + // If we've backspaced past the beginning, cancel unless no key if (caretPosition <= completeStart && options.key) { closeAutocomplete(); - return false; + return true; } // Keyboard codes! So 80's. switch (e.which) { case keys.enter: - case keys.rightArrow: case keys.tab: if (!autocompleteOptions) return true; if (selectedOption >= 0 && (userToComplete = autocompleteOptions[selectedOption])) { From 796a0db98b3f13bf83d5807a68bf79ac39deb0ad Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 8 Feb 2016 00:38:44 +0530 Subject: [PATCH 035/140] FEATURE: add 'New Topic' button on full page search view --- .../discourse/routes/full-page-search.js.es6 | 12 ++++++++++++ .../discourse/templates/full-page-search.hbs | 5 +++++ app/assets/stylesheets/common/base/search.scss | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 index 3cda47e9a7..dc895f4307 100644 --- a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 @@ -1,4 +1,5 @@ import { translateResults, getSearchKey, isValidSearchTerm } from "discourse/lib/search"; +import Composer from 'discourse/models/composer'; export default Discourse.Route.extend({ queryParams: { q: {}, context_id: {}, context: {}, skip_context: {} }, @@ -39,6 +40,17 @@ export default Discourse.Route.extend({ didTransition() { this.controllerFor("full-page-search")._showFooter(); return true; + }, + + createTopic(searchTerm) { + let category; + if (searchTerm.indexOf("category:")) { + const match = searchTerm.match(/category:(\S*)/); + if (match && match[1]) { + category = match[1]; + } + } + this.container.lookup('controller:composer').open({action: Composer.CREATE_TOPIC, draftKey: Composer.CREATE_TOPIC, topicCategory: category}); } } diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index 344c28985b..e216f7ab3f 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -1,6 +1,11 @@ -
+
{{{preview}}}
+ {{#if site.mobileView}} + {{d-button action='hidePreview' class='hide-preview' label='composer.hide_preview'}} + {{/if}}
diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index bc3fa3270f..06ebfdfbef 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -11,6 +11,10 @@ {{render "composer-messages"}}
+ + {{#if site.mobileView}} + + {{/if}} {{#if model.viewOpen}} @@ -20,9 +24,11 @@
{{{model.actionTitle}}} + {{#unless site.mobileView}} {{#if model.whisper}} ({{i18n "composer.whisper"}}) {{/if}} + {{/unless}} {{#if canEdit}} {{#if showEditReason}} @@ -85,6 +91,7 @@ groupsMentioned="groupsMentioned" importQuote="importQuote" showOptions="showOptions" + showToolbar=showToolbar showUploadSelector="showUploadSelector"}} {{#if currentUser}} @@ -92,6 +99,12 @@ {{plugin-outlet "composer-fields-below"}} {{i18n 'cancel'}} + + {{#if site.mobileView}} + {{#if model.whisper}} + + {{/if}} + {{/if}}
{{/if}}
diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index aac01b2e9a..d7372c53a3 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -36,7 +36,7 @@ input { bottom: 0; font-size: 1em; position: fixed; - .toggler { + .toggle-toolbar, .toggler { width: 15px; right: 1px; position: absolute; @@ -48,6 +48,14 @@ input { content: "\f078"; } } + + .toggle-toolbar { + right: 30px; + &:before { + content: "\f0c9"; + } + } + a.cancel { padding-left: 7px; line-height: 30px; @@ -56,7 +64,7 @@ input { margin: 0 0 0 5px; .reply-to { overflow: hidden; - max-width: 92%; + max-width: 80%; white-space: nowrap; i { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); @@ -235,6 +243,31 @@ input { .d-editor-preview-wrapper { display: none; } + + .d-editor-preview-wrapper.force-preview { + display: block; + position: fixed; + z-index: 1000000; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: $secondary; + + .d-editor-preview { + height: 90%; + height: calc(100% - 60px); + border: 0; + overflow: auto; + } + + .hide-preview { + position: fixed; + right: 5px; + bottom: 5px; + z-index: 1000001; + } + } .d-editor-input { width: 100%; height: 100%; @@ -260,6 +293,40 @@ input { .d-editor-button-bar { display: none; } + + + .wmd-controls.toolbar-visible .d-editor-input { + padding-top: 40px; + } + + .wmd-controls.toolbar-visible .d-editor-button-bar { + + .btn.link, .btn.upload, .btn.rule, .btn.bullet, .btn.list, .btn.heading { + display: none; + } + + display: block; + margin: 1px 4px; + position: absolute; + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + background-color: $secondary; + z-index: 100; + overflow: hidden; + width: 100%; + width: calc(100% - 10px); + + -moz-box-sizing: border-box; + box-sizing: border-box; + + button { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + button.btn.no-text { + margin: 0 2px; + padding: 2px 5px; + position: static; + } + } } // make sure the category selector *NEVER* gets focus by default on mobile anywhere From 82a75c00c02534fb111b2834e6ba9e8ceedf688b Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 9 Feb 2016 13:39:10 +0530 Subject: [PATCH 042/140] UX: change 'Visit Topic' to 'Visit Message' for message notification email --- config/locales/server.en.yml | 2 ++ lib/email/message_builder.rb | 4 ++-- spec/components/email/message_builder_spec.rb | 2 +- spec/mailers/user_notifications_spec.rb | 6 ++++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 482721e68b..0c70dfc9db 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1984,6 +1984,8 @@ en: header_instructions: '' reply_by_email: "[Visit Topic](%{base_url}%{url}) or reply to this email to respond" visit_link_to_respond: "[Visit Topic](%{base_url}%{url}) to respond" + reply_by_email_pm: "[Visit Message](%{base_url}%{url}) or reply to this email to respond" + visit_link_to_respond_pm: "[Visit Message](%{base_url}%{url}) to respond" posted_by: "Posted by %{username} on %{post_date}" diff --git a/lib/email/message_builder.rb b/lib/email/message_builder.rb index 28b38f9fa5..76754654ce 100644 --- a/lib/email/message_builder.rb +++ b/lib/email/message_builder.rb @@ -34,9 +34,9 @@ module Email @template_args[:respond_instructions] = '' else @template_args[:respond_instructions] = if allow_reply_by_email? - I18n.t('user_notifications.reply_by_email', @template_args) + @opts[:private_reply] ? I18n.t('user_notifications.reply_by_email_pm', @template_args) : I18n.t('user_notifications.reply_by_email', @template_args) else - I18n.t('user_notifications.visit_link_to_respond', @template_args) + @opts[:private_reply] ? I18n.t('user_notifications.visit_link_to_respond_pm', @template_args) : I18n.t('user_notifications.visit_link_to_respond', @template_args) end end end diff --git a/spec/components/email/message_builder_spec.rb b/spec/components/email/message_builder_spec.rb index 8ff2dc5114..05a4abd92d 100644 --- a/spec/components/email/message_builder_spec.rb +++ b/spec/components/email/message_builder_spec.rb @@ -194,7 +194,7 @@ describe Email::MessageBuilder do end it "does not add unsubscribe via email link without site setting set" do - expect(message_with_unsubscribe_via_email.body).to_not match(/mailto:reply@#{Discourse.current_hostname}\?subject=unsubscribe/) + expect(message_with_unsubscribe_via_email.body).to_not match(/mailto:reply@#{Discourse.current_hostname}\?subject=unsubscribe/) end end diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index 5cb260787b..3934849544 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -111,6 +111,9 @@ describe UserNotifications do # subject should include category name expect(mail.subject).to match(/India/) + # 2 "visit topic" link + expect(mail.html_part.to_s.scan(/Visit Topic/).count).to eq(2) + # 2 respond to links cause we have 1 context post expect(mail.html_part.to_s.scan(/to respond/).count).to eq(2) @@ -181,6 +184,9 @@ describe UserNotifications do # subject should include "[PM]" expect(mail.subject).to match("[PM]") + # 1 "visit message" link + expect(mail.html_part.to_s.scan(/Visit Message/).count).to eq(1) + # 1 respond to link expect(mail.html_part.to_s.scan(/to respond/).count).to eq(1) From af582a8ba93885a7d79d57f226e003f657020c41 Mon Sep 17 00:00:00 2001 From: Prayag Verma Date: Tue, 9 Feb 2016 15:17:24 +0530 Subject: [PATCH 043/140] Fix a typo Misspelled `Atlassian` as `Atlassan` --- docs/DEVELOPMENT-OSX-NATIVE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DEVELOPMENT-OSX-NATIVE.md b/docs/DEVELOPMENT-OSX-NATIVE.md index 1b2536c23b..4a6a7259a6 100644 --- a/docs/DEVELOPMENT-OSX-NATIVE.md +++ b/docs/DEVELOPMENT-OSX-NATIVE.md @@ -93,7 +93,7 @@ You should now be able to check out a clone of Discourse. ### SourceTree -Atlassan has a free Git client for OS X called [SourceTree](http://www.sourcetreeapp.com/download/) which can be extremely useful for keeping visual track of what's going on in Git-land. While it's arguably not a full substitute for command-line git (especially if you know the command line well), it's extremely powerful for a GUI version-control client. +Atlassian has a free Git client for OS X called [SourceTree](http://www.sourcetreeapp.com/download/) which can be extremely useful for keeping visual track of what's going on in Git-land. While it's arguably not a full substitute for command-line git (especially if you know the command line well), it's extremely powerful for a GUI version-control client. ## Postgres 9.3 From 35142847ba3968d2a8e70357b20d1a2b116bccd1 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sat, 26 Sep 2015 15:56:36 +0200 Subject: [PATCH 044/140] FIX: Prepend the user id before username in admin user routes --- app/assets/javascripts/admin/models/admin-user.js.es6 | 8 ++++---- .../javascripts/admin/routes/admin-route-map.js.es6 | 2 +- app/assets/javascripts/admin/routes/admin-user.js.es6 | 4 ++-- app/assets/javascripts/discourse/controllers/flag.js.es6 | 3 +-- app/assets/javascripts/discourse/controllers/user.js.es6 | 3 +-- app/assets/javascripts/discourse/models/user.js.es6 | 2 +- app/controllers/admin/users_controller.rb | 2 +- config/routes.rb | 5 +++-- spec/controllers/admin/users_controller_spec.rb | 4 ++-- 9 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 267d951e56..14b7e3edc3 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -402,7 +402,7 @@ const AdminUser = Discourse.User.extend({ } } }).catch(function() { - AdminUser.find( user.get('username') ).then(function(u){ user.setProperties(u); }); + AdminUser.find(user.get('id')).then(u => user.setProperties(u)); bootbox.alert(I18n.t("admin.user.delete_failed")); }); }; @@ -475,7 +475,7 @@ const AdminUser = Discourse.User.extend({ if (user.get('loadedDetails')) { return Ember.RSVP.resolve(user); } - return AdminUser.find(user.get('username_lower')).then(function (result) { + return AdminUser.find(user.get('id')).then(result => { user.setProperties(result); user.set('loadedDetails', true); }); @@ -533,8 +533,8 @@ AdminUser.reopenClass({ }); }, - find(username) { - return Discourse.ajax("/admin/users/" + username + ".json").then(function (result) { + find(user_id) { + return Discourse.ajax("/admin/users/" + user_id + ".json").then(result => { result.loadedDetails = true; return AdminUser.create(result); }); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index 3626ea48d3..64a8e393a0 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -62,7 +62,7 @@ export default { }); this.resource('adminUsers', { path: '/users' }, function() { - this.resource('adminUser', { path: '/:username' }, function() { + this.resource('adminUser', { path: '/:user_id/:username' }, function() { this.route('badges'); this.route('tl3Requirements', { path: '/tl3_requirements' }); }); diff --git a/app/assets/javascripts/admin/routes/admin-user.js.es6 b/app/assets/javascripts/admin/routes/admin-user.js.es6 index af3171a857..35a105a105 100644 --- a/app/assets/javascripts/admin/routes/admin-user.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-user.js.es6 @@ -2,11 +2,11 @@ import AdminUser from 'admin/models/admin-user'; export default Discourse.Route.extend({ serialize(model) { - return { username: model.get('username').toLowerCase() }; + return { user_id: model.get('id'), username: model.get('username').toLowerCase() }; }, model(params) { - return AdminUser.find(Em.get(params, 'username').toLowerCase()); + return AdminUser.find(Em.get(params, 'user_id')); }, renderTemplate() { diff --git a/app/assets/javascripts/discourse/controllers/flag.js.es6 b/app/assets/javascripts/discourse/controllers/flag.js.es6 index 07db5951b1..ea3bc834e2 100644 --- a/app/assets/javascripts/discourse/controllers/flag.js.es6 +++ b/app/assets/javascripts/discourse/controllers/flag.js.es6 @@ -141,8 +141,7 @@ export default Ember.Controller.extend(ModalFunctionality, { fetchUserDetails() { if (Discourse.User.currentProp('staff') && this.get('model.username')) { const AdminUser = require('admin/models/admin-user').default; - AdminUser.find(this.get('model.username').toLowerCase()) - .then(user => this.set('userDetails', user)); + AdminUser.find(this.get('model.id')).then(user => this.set('userDetails', user)); } } diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 3971558f88..64202d0985 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -84,8 +84,7 @@ export default Ember.Controller.extend(CanCheckEmails, { adminDelete() { // I really want this deferred, don't want to bring in all this code till used const AdminUser = require('admin/models/admin-user').default; - AdminUser.find(this.get('model.username').toLowerCase()) - .then(user => user.destroy({deletePosts: true})); + AdminUser.find(this.get('model.id')).then(user => user.destroy({deletePosts: true})); }, } diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 442899731e..fd702adff7 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -90,7 +90,7 @@ const User = RestModel.extend({ }, - adminPath: url('username_lower', "/admin/users/%@"), + adminPath: url('id', 'username_lower', "/admin/users/%@1/%@2"), mutedTopicsPath: url('/latest?state=muted'), diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 36aa18f19f..da18abe08d 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -37,7 +37,7 @@ class Admin::UsersController < Admin::AdminController end def show - @user = User.find_by(username_lower: params[:id]) + @user = User.find_by(id: params[:id]) raise Discourse::NotFound unless @user render_serialized(@user, AdminDetailedUserSerializer, root: false) end diff --git a/config/routes.rb b/config/routes.rb index 49d7a623e8..d0457c90c4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -73,8 +73,7 @@ Discourse::Application.routes.draw do get "groups/:type" => "groups#show", constraints: AdminConstraint.new get "groups/:type/:id" => "groups#show", constraints: AdminConstraint.new - get "users/:id.json" => 'users#show' , id: USERNAME_ROUTE_FORMAT, defaults: {format: 'json'} - resources :users, id: USERNAME_ROUTE_FORMAT do + resources :users, id: USERNAME_ROUTE_FORMAT, except: [:show] do collection do get "list/:query" => "users#index" get "ip-info" => "users#ip_info" @@ -109,6 +108,8 @@ Discourse::Application.routes.draw do get "tl3_requirements" put "anonymize" end + get "users/:id.json" => 'users#show', defaults: {format: 'json'} + get 'users/:id/:username' => 'users#show' post "users/sync_sso" => "users#sync_sso", constraints: AdminConstraint.new diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index c35c83d3c2..b3de180969 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -47,14 +47,14 @@ describe Admin::UsersController do describe '.show' do context 'an existing user' do it 'returns success' do - xhr :get, :show, id: @user.username + xhr :get, :show, id: @user.id expect(response).to be_success end end context 'an existing user' do it 'returns success' do - xhr :get, :show, id: 'foobar' + xhr :get, :show, id: 0 expect(response).not_to be_success end end From e5aecdf09f029c29f26136b5791f975abb4cccc9 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 9 Feb 2016 20:20:34 +0530 Subject: [PATCH 045/140] Update Translations --- config/locales/client.ar.yml | 1 - config/locales/client.da.yml | 1 - config/locales/client.fa_IR.yml | 1 - config/locales/client.fr.yml | 11 +++- config/locales/client.he.yml | 1 - config/locales/client.it.yml | 5 +- config/locales/client.ja.yml | 1 - config/locales/client.nl.yml | 1 - config/locales/client.pl_PL.yml | 1 - config/locales/client.pt.yml | 1 - config/locales/client.pt_BR.yml | 1 - config/locales/client.ro.yml | 1 - config/locales/client.ru.yml | 113 ++++++++++++++++++++------------ config/locales/client.sk.yml | 1 - config/locales/client.sq.yml | 1 - config/locales/client.tr_TR.yml | 1 - config/locales/client.zh_CN.yml | 1 - config/locales/server.bs_BA.yml | 13 ---- config/locales/server.it.yml | 6 +- config/locales/server.nb_NO.yml | 1 - config/locales/server.ro.yml | 16 ----- config/locales/server.sv.yml | 1 - config/locales/server.te.yml | 1 - 23 files changed, 86 insertions(+), 95 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 69bbf097fc..0dd9c71b55 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -533,7 +533,6 @@ ar: not_supported: "عذراً , الإشعارات غير مدعومة على هذا المتصفح " perm_default: "تفعيل الإشعارات" perm_denied_btn: "الصلاحيات ممنوعة " - perm_denied_expl: "لقد قمت بإيقاف صلاحية الإشعارات في متصفحك . إستخدم متصفحك لتفعيل التنبيهات و ثم أعد ضغط الزر .\n( سطح المكتب : الأيقونة في أقصى اليسار في شريط العنوان . للهواتف الذكية : في معلومات الموقع Site Info)" disable: "إيقاف الإشعارات " currently_enabled: "( مفعل مسبقاً )" enable: "تفعيل الإشعارات" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 150b020ae0..9c65da8741 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -392,7 +392,6 @@ da: not_supported: "Notifikationer understøttes ikke af denne browser. Beklager." perm_default: "Slå notifikationer til" perm_denied_btn: "Tilladelse nægtet" - perm_denied_expl: "Du har ikke givet tilladelse til notifikationer. Brug din browser til at aktivere notifikationer og klik derefter på knappen, når du er færdig. (Computer: Ikonet længst til venstre i adresselinjen. Mobil: 'Site info'.)" disable: "Deaktiver notifikationer" currently_enabled: "(slået til)" enable: "Aktiver notifikationer" diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index ec7187d821..ae24711f45 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -383,7 +383,6 @@ fa_IR: not_supported: "اعلانات بر روی این مرورگر پشتیبانی نمیشوند. با عرض پوزش." perm_default: "فعال کردن اعلانات" perm_denied_btn: "دسترسی رد شد" - perm_denied_expl: "شما دسترسی به اعلانات ندارید. از مرورگر خود برای فعال کردن اعلانات استفاده کنید, سپس بر روی دکمه کلیک کنید. (دسکتاپ: آیکون سمت چپ داخل نوار آدرس. تلفن همراه: 'اطلاعات وبسایت'.)" disable: "غیرفعال کردن اعلانات" currently_enabled: "(در حال حاضر فعال است)" enable: "فعال کردن اعلانات" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 082d0bc284..3600bedc89 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -428,7 +428,7 @@ fr: not_supported: "Les notifications ne sont pas supportées avec ce navigateur. Désolé." perm_default: "Activer les notifications" perm_denied_btn: "Permission Refusée" - perm_denied_expl: "Vous avez refusé la permission pour les notifications. Utilisez votre navigateur pour activer les notifications, puis appuyez sur le bouton une fois terminé. (Bureau : L'icône la plus à gauche dans la barre d'adresse. Mobile : 'Info Site'.)" + perm_denied_expl: "Vous n'avez pas autorisé les notifications. Autorisez-les depuis les paramètres de votre navigateur." disable: "Désactiver les notifications" currently_enabled: "(activé actuellement)" enable: "Activer les notifications" @@ -482,6 +482,7 @@ fr: groups: "Mes groupes" bulk_select: "Sélection des messages" move_to_inbox: "Déplacer dans la boîte de réception" + move_to_archive: "Archiver" failed_to_move: "Impossible de déplacer les messages sélectionnés (peut-être que votre connexion est coupée)" select_all: "Sélectionner tout" change_password: @@ -909,6 +910,9 @@ fr: moved_post: "

{{username}} a déplacé {{description}}

" linked: "

{{username}} {{description}}

" granted_badge: "

Vous avez gagné {{description}}

" + group_message_summary: + one: "

{{count}} message dans votre boite de réception {{group_name}}

" + other: "

{{count}} messages dans votre boite de réception {{group_name}}

" alt: mentioned: "Mentionné par" quoted: "Cité par" @@ -923,6 +927,7 @@ fr: moved_post: "Votre message a été déplacé par" linked: "Lien vers votre message" granted_badge: "Badge attribué" + group_message_summary: "Messages dans la boite de réception de groupe." popup: mentioned: '{{username}} vous a mentionné dans «{{topic}}» - {{site_title}}' group_mentioned: '{{username}} vous a mentionné dans «{{topic}}» - {{site_title}}' @@ -2146,6 +2151,10 @@ fr: create_category: "créer une catégorie" block_user: "bloquer l'utilisateur" unblock_user: "débloquer l'utilisateur" + grant_admin: "Accorder les droits d'admin" + revoke_admin: "Révoquer les droits d'admin" + grant_moderation: "Accorder les droits de modération" + revoke_moderation: "Révoquer les droits de modération" screened_emails: title: "Courriels affichés" description: "Lorsque quelqu'un essaye de créé un nouveau compte, les adresses de courriel suivantes seront vérifiées et l'inscription sera bloquée, ou une autre action sera réalisée." diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 5265ee88f5..dcd24e4f39 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -418,7 +418,6 @@ he: not_supported: "התראות לא נתמכות בדפדפן זה. מצטערים." perm_default: "הדלק התראות" perm_denied_btn: "הרשאות נדחו" - perm_denied_expl: "נטרלת הראשות עבור התראות. השתמש בדפדפן שלך לאפשר התראות, לאחר מכן לחץ על הכפתור. " disable: "כבה התראות" currently_enabled: "(כרגע מאופשר)" enable: "אפשר התראות" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index cf4657a9a2..d76d658f01 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -415,7 +415,6 @@ it: not_supported: "Spiacenti, le notifiche non sono supportate su questo browser." perm_default: "Attiva Notifiche" perm_denied_btn: "Permesso Negato" - perm_denied_expl: "Hai negato il permesso per le notifiche. Usa il browser per abilitare le notifiche, poi premi il bottone quando hai finito. (Per il desktop: è l'icona più a sinistra sulla barra degli indirizzi. Mobile: 'Informazioni sul sito'.)" disable: "Disabilita Notifiche" currently_enabled: "(attualmente attivate)" enable: "Abilita Notifiche" @@ -798,6 +797,7 @@ it: ctrl: 'Ctrl' alt: 'Alt' composer: + emoji: "Emoji :)" more_emoji: "altro..." options: "Opzioni" whisper: "sussurra" @@ -966,7 +966,7 @@ it: dismiss_read: "Chiudi tutti i non letti" dismiss_button: "Chiudi..." dismiss_tooltip: "Chiudi solo gli ultimi messaggi o smetti di seguire gli argomenti" - also_dismiss_topics: "Smetti di tracciare questi topic così che non compaiano più come non letti" + also_dismiss_topics: "Smetti di seguire questi argomenti così che non compariranno più come non letti per me" dismiss_new: "Chiudi Nuovo" toggle: "commuta la selezione multipla degli argomenti" actions: "Azioni Multiple" @@ -2018,6 +2018,7 @@ it: description: "Colore base usato per lo sfondo dei messaggi wiki." email: settings: "Impostazioni" + templates: "Template" preview_digest: "Anteprima Riassunto" sending_test: "Invio email di prova in corso..." error: "ERRORE - %{server_error}" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 2e20c9e858..9ea38f58cc 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -332,7 +332,6 @@ ja: not_supported: "申し訳ありません。そのブラウザは通知をサポートしていません。" perm_default: "通知をONにする" perm_denied_btn: "アクセス拒否" - perm_denied_expl: "通知へのアクセスを拒否されています。利用しているブラウザの通知を有効にし、完了したら、このボタンをクリックしてください。(デスクトップ: アドレスバーの左端のアイコン。 モバイル: \"サイト情報\")" disable: "通知を無効にする" currently_enabled: "(現在有効化されています)" enable: "通知を有効化" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 6ae3982245..daab5b8e4b 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -393,7 +393,6 @@ nl: not_supported: "Notificaties worden niet ondersteund door deze browser. Sorry." perm_default: "Notificaties aanzetten" perm_denied_btn: "Toestemming geweigerd" - perm_denied_expl: "Gebruik van notificaties staat niet ingeschakeld. Gebruik je browser om notificaties toe te staan, klik vervolgens op de knop. (Desktop: Het meest linkse icoon op de adresbalk. Mobiel: 'Site Info' )" disable: "Notificaties uitschakelen" currently_enabled: "(momenteel ingeschakeld)" enable: "Notificaties inschakelen" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 4cbe07bdd7..2d9005ccb5 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -422,7 +422,6 @@ pl_PL: not_supported: "Powiadomienia nie są wspierane przez tę przeglądarkę. Przepraszamy." perm_default: "Włącz powiadomienia" perm_denied_btn: "Brak uprawnień" - perm_denied_expl: "Wyświetlanie powiadomień jest zablokowane. Użyj ustawień swojej przeglądarki, aby odblokować powiadomienia dla tej domeny." disable: "Wyłącz powiadomienia" currently_enabled: "(aktualnie włączone)" enable: "Włącz powiadomienia" diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 80d67fe793..a555464c5e 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -413,7 +413,6 @@ pt: not_supported: "Não são suportadas notificações neste navegador. Desculpe." perm_default: "Ligar Notificações" perm_denied_btn: "Permissão Negada" - perm_denied_expl: "Tem permissões negadas para notificações. Utilize o seu navegador para ativar notificações, de seguida clique no botão quando concluir. (Desktop: O ícone mais à esquerda na barra de endereço. Móvel: 'Info do Sítio'.)" disable: "Desativar Notificações" currently_enabled: "(atualmente ativo)" enable: "Ativar Notificações" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 69df78a392..215061353a 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -391,7 +391,6 @@ pt_BR: not_supported: "Notificações não são suportadas nesse browser. Desculpe-nos." perm_default: "Habilitar Notificações" perm_denied_btn: "Permissão Negada" - perm_denied_expl: "Você negou permissões para as notificações. Utilize seu navegador para habilitar notificações, e depois, clique no botão. (Desktop: O ícone mais a esquerda da barra de endereços. Dispositivos Móveis: 'Configurações do Site')" disable: "Desativar Notificações" currently_enabled: "(atualmente ativado)" enable: "Ativar Notificações" diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index ead8f09682..7e4c3935dc 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -404,7 +404,6 @@ ro: not_supported: "Notificarile nu sunt suportate in acest browser. Scuze." perm_default: "Activeaza notificarile" perm_denied_btn: "Nu se permite accesul" - perm_denied_expl: "Ai blocat permisia pentru notificari. Utilieaza un browser pentru a le activa, apoi apasa butonul cand este gata. (Desktop: Iconita din stanga barei de adrese. Mobil: 'site Info'.)" disable: "Dezactiveaza notificarile" currently_enabled: "(acum activat)" enable: "Activeaza Notificarile" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 04f53e44fc..7d8afc6506 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -470,7 +470,6 @@ ru: not_supported: "К сожалению, оповещения не поддерживаются этим браузером." perm_default: "Включить оповещения" perm_denied_btn: "Отказано в разрешении" - perm_denied_expl: "Вы запретили оповещения в вашем браузере. Вначале возобновите разрешение, а затем попробуйте еще раз." disable: "Отключить оповещения" currently_enabled: "(сейчас включены)" enable: "Включить оповещения" @@ -684,6 +683,21 @@ ru: same_as_email: "Ваш пароль такой же, как и ваш email." ok: "Допустимый пароль." instructions: "Не менее %{count} символов." + summary: + title: "Сводка" + stats: "Статистика" + topic_count: "Создал тем" + post_count: "Написал сообщений" + likes_given: "Выразил симпатий" + likes_received: "Получил симпатий" + days_visited: "Заходил на форум дней" + posts_read_count: "Прочитал сообщений" + top_replies: "Лучшие сообщения" + top_topics: "Лучшие темы" + top_badges: "Самые престижные награды" + more_topics: "... другие темы" + more_replies: "... другие сообщения" + more_badges: "... другие награды" associated_accounts: "Связанные аккаунты" ip_address: title: "Последний IP адрес" @@ -890,29 +904,29 @@ ru: link_optional_text: "текст ссылки" link_placeholder: "Пример: http://example.com \"текст ссылки\"" quote_title: "Цитата" - quote_text: "Цитата" - code_title: "Форматированный текст" - code_text: "добавьте 4 символа пробела, перед форматированным текстом" + quote_text: "Впишите текст цитаты сюда" + code_title: "Текст \"как есть\" (без применения форматирования)" + code_text: "впишите текст сюда; также, отключить форматирование текста можно, начав строку с четырех пробелов" upload_title: "Загрузить" - upload_description: "введите здесь описание загружаемого объекта" + upload_description: "введите описание загружаемого объекта" olist_title: "Нумерованный список" - ulist_title: "Маркированный список" - list_item: "Элемент списка" + ulist_title: "Ненумерованный список" + list_item: "Пункт первый" heading_title: "Заголовок" heading_text: "Заголовок" hr_title: "Горизонтальный разделитель" - help: "Справка по Markdown" + help: "Справка по форматированию (Markdown)" toggler: "скрыть / показать панель редактирования" modal_ok: "OK" modal_cancel: "Отмена" - cant_send_pm: "Извините, но вы не можете отправлять сообщения пользователю %{username}." + cant_send_pm: "К сожалению, вы не можете отправлять сообщения пользователю %{username}." admin_options_title: "Дополнительные настройки темы" auto_close: label: "Закрыть тему через:" error: "Пожалуйста, введите корректное значение." based_on_last_post: "Не закрывать, пока не пройдет хотя бы такой промежуток времени с момента последнего сообщения в этой теме." all: - examples: 'Введите количество часов (24), абсолютное время (17:30), или дату и время (2013-11-22 14:00).' + examples: 'Введите количество часов (напр., 24), время (напр., 17:30) или дату и время (2013-11-22 14:00).' limited: units: "(кол-во часов)" examples: 'Введите количество часов (24).' @@ -1003,6 +1017,7 @@ ru: dismiss_read: "Отклонить все непрочитанные" dismiss_button: "Отложить..." dismiss_tooltip: "Отложить новые сообщения или перестать следить за этими темами" + also_dismiss_topics: "Перестать следить за этими темами, чтобы они никогда больше не высвечивались как непрочитанные" dismiss_new: "Отложить новые" toggle: "Вкл./выкл. выбор нескольких тем" actions: "Массовое действие" @@ -1104,7 +1119,7 @@ ru: toggle_information: "скрыть / показать подробную информацию о теме" read_more_in_category: "Хотите почитать что-нибудь еще? Можно посмотреть темы в {{catLink}} или {{latestLink}}." read_more: "Хотите почитать что-нибудь еще? {{catLink}} или {{latestLink}}." - read_more_MF: "У вас { UNREAD, plural, =0 {} one { 1 непрочитанное } other { # непрочитанных } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false { } other{}} 1 новая тема} other { {BOTH, select, true{and } false { } other{}} # новых тем} } осталось, или {CATEGORY, select, true {посмотреть другие темы в{catLink}} false {{latestLink}} other {}}" + read_more_MF: "У вас { UNREAD, plural, =0 {} one { 1 непрочитанная } other { # непрочитанных } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false { } other{}} 1 новая тема} other { {BOTH, select, true{and } false { } other{}} # новых тем} } осталось, или {CATEGORY, select, true {посмотрите другие темы в разделе {catLink}} false {{latestLink}} other {}}" browse_all_categories: Просмотреть все разделы view_latest_topics: посмотреть последние темы suggest_create_topic: Почему бы вам не создать новую тему? @@ -1300,6 +1315,8 @@ ru: many: Вы выбрали {{count}} сообщений. other: Вы выбрали {{count}} сообщений. post: + reply: " {{replyAvatar}} {{usernameLink}}" + reply_topic: " {{link}}" quote_reply: "ответить цитированием" edit: "Изменить {{link}} {{replyAvatar}} {{username}}" edit_reason: "Причина:" @@ -1759,11 +1776,13 @@ ru: help: "последние темы в разделе {{categoryName}}" top: title: "Обсуждаемые" - help: "наиболее активные темы за прошлый год, месяц, неделю или день" + help: "Самые активные темы за последний год, месяц, квартал, неделю или день" all: title: "За все время" yearly: title: "За год" + quarterly: + title: "За квартал" monthly: title: "За месяц" weekly: @@ -1771,12 +1790,12 @@ ru: daily: title: "За день" all_time: "За все время" - this_year: "Год" - this_quarter: "Квартал" - this_month: "Месяц" - this_week: "Неделя" - today: "Сегодня" - other_periods: "просмотреть выше" + this_year: "За год" + this_quarter: "За квартал" + this_month: "За месяц" + this_week: "За неделю" + today: "За сегодня" + other_periods: "показать самые обсуждаемые" browser_update: 'К сожалению, ваш браузер устарел и не поддерживается этим сайтом. Пожалуйста, обновите браузер (нажмите на ссылку, чтобы узнать больше).' permission_types: full: "Создавать / Отвечать / Просматривать" @@ -2178,27 +2197,32 @@ ru: no_previous: "Старое значение отсутствует." deleted: "Новое значение отсутствует. Запись была удалена." actions: - delete_user: "удаление пользователя" - change_trust_level: "изменение уровня доверия" - change_username: "изменение псевдонима" - change_site_setting: "изменение настройки сайта" - change_site_customization: "изменение настройки сайта" - delete_site_customization: "удаление настроек сайта" - suspend_user: "заморозка пользователя" - unsuspend_user: "разморозка пользователя" - grant_badge: "выдача награды" - revoke_badge: "отозыв награды" - check_email: "открыть e-mail" - delete_topic: "удаление темы" - delete_post: "удаление сообщения" - impersonate: "выдавание себя за" - anonymize_user: "анонимизация пользователя" - roll_up: "группирование IP адресов в подсети" - change_category_settings: "изменение настроек раздела" - delete_category: "удаление раздела" - create_category: "создание раздела" - block_user: "заблокировать пользователя" - unblock_user: "Разблокировать пользователя" + delete_user: "удален пользователь" + change_trust_level: "изменен уровень доверия" + change_username: "изменен псевдоним" + change_site_setting: "изменена настройка сайта" + change_site_customization: "изменена настройка сайта" + delete_site_customization: "удалена настройка сайта" + change_site_text: "изменен текст" + suspend_user: "пользователь заморожен" + unsuspend_user: "пользователь разморожен" + grant_badge: "выдана награда" + revoke_badge: "отозвана награда" + check_email: "доступ к адресу e-mail" + delete_topic: "удалена тема" + delete_post: "удалено сообщение" + impersonate: "вход от имени пользователя" + anonymize_user: "пользователь анонимизрован" + roll_up: "сгруппированы заблокированные IP адреса в подсеть" + change_category_settings: "изменена настройка раздела" + delete_category: "удален раздел" + create_category: "создан раздел" + block_user: "пользователь заблокирован" + unblock_user: "пользователь разблокирован" + grant_admin: "выданы права администратора" + revoke_admin: "отозваны права администратора" + grant_moderation: "выданы права модератора" + revoke_moderation: "отозваны права модератора" screened_emails: title: "Почтовые адреса" description: "Когда кто-то создает новую учетную запись, проверяется данный почтовый адрес и регистрация блокируется или производятся другие дополнительные действия." @@ -2572,16 +2596,19 @@ ru: save: "Сохранить настройки встраивания" permalink: title: "Постоянные ссылки" - url: "URL" - topic_id: "ID темы" + url: "Ссылка URL" + topic_id: "Номер темы" topic_title: "Тема" - post_id: "ID сообщения" + post_id: "Номер сообщения" post_title: "Сообщение" - category_id: "ID раздела" + category_id: "Номер раздела" category_title: "Раздел" external_url: "Внешняя ссылка" + delete_confirm: Удалить эту постоянную ссылку? form: + label: "Новая постоянная ссылка:" add: "Добавить" + filter: "Поиск по ссылке или внешней ссылке (URL)" lightbox: download: "загрузить" search_help: diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 987bc3c596..f8b621c609 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -458,7 +458,6 @@ sk: not_supported: "Tento prehliadač nepodporuje upozornenia. Prepáčte." perm_default: "Zapnúť upozornenia" perm_denied_btn: "Prístup zamietnutý" - perm_denied_expl: "Notifikácie nie sú povolené. Povoľte notifikácie vo vašom prehliadači a potom kliknite na tlačidlo. (Desktop: ľavá krajná ikona v adresnom riadku. Mobil: 'Site info')" disable: "Zakázať upozornenia" currently_enabled: "(momentálne povolené)" enable: "Povoliť upozornenia" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index db47bcf91f..d8fe81d4fa 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -375,7 +375,6 @@ sq: not_supported: "Notifications are not supported on this browser. Sorry." perm_default: "Turn On Notifications" perm_denied_btn: "Permission Denied" - perm_denied_expl: "You have denied permission for notifications. Use your browser to enable notifications, then click the button when done. (Desktop: The leftmost icon in the address bar. Mobile: 'Site Info'.)" disable: "Disable Notifications" currently_enabled: "(currently enabled)" enable: "Enable Notifications" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 223f953a71..dd61fc96fb 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -380,7 +380,6 @@ tr_TR: not_supported: "Bildirimler bu tarayıcıda desteklenmiyor. Üzgünüz." perm_default: "Bildirimleri Etkinleştirin" perm_denied_btn: "Erişim İzni Reddedildi" - perm_denied_expl: "Bildirimler için gerekli izne sahip değilsiniz. Bildirimleri etkinleştirmek için tarayıcınızı kullanın, işlem tamamlandığında tuşa basın. (Masaüstü: Adres çubuğunda en soldaki simge. Mobil: 'Site Bilgisi'.)" disable: "Bildirimleri Devre Dışı Bırakın" currently_enabled: "(şu anda etkin)" enable: "Bildirimleri Etkinleştirin" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 35977e24cb..d34227a06d 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -379,7 +379,6 @@ zh_CN: not_supported: "通知功能暂不支持该浏览器。抱歉。" perm_default: "启用通知" perm_denied_btn: "拒绝授权" - perm_denied_expl: "你拒绝了通知权限。在你的浏览器中允许启用通知,然后再点击按钮。(桌面:点击最左边的图标。移动设备:“站点设置”。)" disable: "禁用通知" currently_enabled: "(目前已启用)" enable: "启用通知" diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index 91916565f7..bcce781f8c 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -397,7 +397,6 @@ bs_BA: max_topic_title_length: "Maximum allowed topic title length in characters" min_private_message_title_length: "Minimum allowed title length for a private message in characters" min_search_term_length: "Minimum valid search term length in characters" - uncategorized_description: "The description of the uncategorized category. Leave blank for no description." allow_duplicate_topic_titles: "Allow topics with identical, duplicate titles." unique_posts_mins: "How many minutes before a user can make a post with the same content again" educate_until_posts: "When the user starts typing their first (n) new posts, show the pop-up new user education panel in the composer." @@ -923,26 +922,14 @@ bs_BA: text_body_template: "The export has failed. Please check the logs." email_reject_no_account: subject_template: "Email issue -- No Account" - text_body_template: | - We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - - There is no known account with this email address. Try sending from a different email address, or contact a staff member. email_reject_empty: subject_template: "Email issue -- No Content" email_reject_parsing: subject_template: "Email issue -- Content unrecognized" email_reject_reply_key: subject_template: "Email issue -- Bad Reply Key" - text_body_template: | - We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - - The provided reply key is invalid or unknown, so we don't know what this email is in reply to. Contact a staff member. email_error_notification: subject_template: "Email issue -- POP authentication error" - text_body_template: | - There has been an authentication error while polling mails from the POP server. - - Please make sure you have properly configured the POP credentials in [the site settings](%{base_url}/admin/site_settings/category/email). too_many_spam_flags: subject_template: "New account blocked" text_body_template: | diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 8f081d8bfe..3d12040e83 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -265,6 +265,7 @@ it: one: "Non puoi eliminare questa categoria perché ha 1 argomento. L'argomento più vecchio è %{topic_link}." other: "Non puoi cancellare questa categoria perché ha %{count} argomenti. L'argomento più vecchio è %{topic_link}." topic_exists_no_oldest: "Non puoi cancellare questa categoria perché il numero di argomenti è di %{count}." + uncategorized_description: "Argomenti che non necessitano di una categoria, o che non ricadono in nessuna categoria esistente." trust_levels: newuser: title: "nuovo utente" @@ -615,7 +616,7 @@ it: host_names_warning: "Il tuo file config/database.yml usa l'hostname di default: localhost. Aggiornalo con l'hostname del tuo sito." gc_warning: 'Il tuo server usa i parametri di garbage collection di default di ruby, che potrebbero non garantirti le migliori prestazioni. Leggi questo argomento sulla taratura delle prestazioni: Tuning Ruby and Rails for Discourse.' sidekiq_warning: 'Sidekiq non è in esecuzione. Molte attività, come l''invio di email, sono eseguite in maniera asincrona da sidekiq. Assicurati che almeno un processo sidekiq sia in esecuzione. Leggi altro su sidekiq qui.' - queue_size_warning: 'Il numero di job in coda è %{queue_size}, il che è alto. Questo potrebbe indicare un problema con i processi Sidekiq, o potrebbe essere necessario aggiungere altri worker Sidekiq.' + queue_size_warning: 'Il numero di job in coda è %{queue_size}, il che è alto. Ciò potrebbe indicare un problema con i processi Sidekiq, oppure devi aggiungere altri worker Sidekiq.' memory_warning: 'Il tuo server gira con meno di 1 GB di memoria. Si raccomanda almeno 1 GB di memoria.' google_oauth2_config_warning: 'Il server è configurato per permettere iscrizioni e login con Google Oauth2 (enable_google_oauth2_logins), ma il client id e il client secret non sono impostati. Vai nelle Impostazioni del sito e aggiorna le impostazioni. Leggi questa guida per saperne di più.' facebook_config_warning: 'Il server è configurato per accettare iscrizioni e login con Facebook (enable_facebook_logins), tuttavia i parametri app id e secret non sono stati impostati. Vai alle Impostazioni e aggiorna i campi interessati. Leggi questa guida per saperne di più.' @@ -649,7 +650,6 @@ it: min_search_term_length: "Numero minimo di caratteri per le parole cercate" search_tokenize_chinese_japanese_korean: "Attiva la tokenizzazione dei caratteri Cinesi/Giapponesi/Coreani nella ricerca anche sui siti non CJK" allow_uncategorized_topics: "Permetti la creazione di argomenti senza categoria. ATTENZIONE: se ci sono argomenti senza categoria, devi ricategorizzarli prima di disabilitare questa opzione." - uncategorized_description: "La descrizione della categoria \"Non classificato\". Lascia vuoto per nessuna descrizione." allow_duplicate_topic_titles: "Permetti più argomenti con lo stesso identico titolo" unique_posts_mins: "Quanti minuti prima che un utente possa inviare un altro argomento con lo stesso contenuto" educate_until_posts: "Per i primi (n) messaggi di un utente, mostra la finestra pop-up con il pannello di istruzioni per nuovi utenti." @@ -919,7 +919,7 @@ it: notify_about_flags_after: "Se ci sono segnalazioni che non sono state revisionate dopo un tale numero di ore, invia un'email al contact_email. Imposta a 0 per disattivare." show_create_topics_notice: "Se il sito ha meno di 5 argomenti pubblici, mostra un avviso chiedendo agli amministratori di creare qualche argomento." enable_emoji: "Attiva gli emoji" - default_other_disable_jump_reply: "Non passare al post dell'utente dopo la sua risposta di default." + default_other_disable_jump_reply: "Per default non saltare al messaggio dell'utente dopo la sua risposta." default_categories_watching: "Elenco di categorie osservate per default." default_categories_tracking: "Elenco di categorie seguite per default." default_categories_muted: "Elenco di categorie silenziate per default." diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index 62ad0be452..62a0449f43 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -594,7 +594,6 @@ nb_NO: min_topic_title_length: "Minimum tillatt lengde for tittel i tegn" max_topic_title_length: "Maksimum tillatt lengde for innlegg i tegn" min_search_term_length: "Minimum lengde på søkeord i tegn" - uncategorized_description: "Beskrivelsen av denne ukategoriserte kategorien. La stå tom for ingen beskrivelse." allow_duplicate_topic_titles: "Tillat emner med lik, duplikat tittel." unique_posts_mins: "Hvor mange minutter før en bruker kan lage en post med det samme innholdet igjen" educate_until_posts: "Når bruker begynner å skrive på de første (n) innleggene, vis pop-up med opplæringspanelet for nye brukere i editoren." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 088408d84a..8904158bbd 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -538,7 +538,6 @@ ro: min_topic_title_length: "Minimul de caractere permis per discuție" max_topic_title_length: "Maximul de caractere permis per discuție" min_search_term_length: "Minimul de caractere permis pentru o căutare validă" - uncategorized_description: "Descrierea categoriei necategorisite. Lăsați liber pentru nicio descriere." allow_duplicate_topic_titles: "Permite discuții cu titluri duplicate sau identice." unique_posts_mins: "Câte minute până un utilizator poate posta iar cu același conținut" educate_until_posts: "Când utilizatorul începe să scrie primele (n) postări, arată panelul de instruire în spațiul pentru compunere." @@ -863,21 +862,6 @@ ro: ``` %{logs} ``` - email_reject_no_account: - text_body_template: | - Ne pare rău, dar emailul trimis de dvs către %{destination} (cu titlul %{former_title}) nu a funcționat. - - Nu există niciun cont corelat cu această adresă Email. Încercați să trimiteți de pe o altă adresa de email, sau contactați un membru al personalului. - email_reject_reply_key: - text_body_template: | - Ne pare rău, dar emailul către %{destination} (cu titlul %{former_title}) nu a funcționat. - - Cheia de răspuns este ori invalidă ori nefuncționabilă, deci nu știm la ce răspunde aces email. Contactați un membru al personalului. - email_error_notification: - text_body_template: | - S-a semnalat o eroare la sustragerea mailurilor din serverul POP. - - Asigurați-vă că ați setat serverul POP în [setările site-ului](%{base_url}/admin/setări_de_site/categorie/email). too_many_spam_flags: subject_template: "Cont nou blocat" text_body_template: | diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 0adff550ce..b5eb9d97c8 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -586,7 +586,6 @@ sv: min_first_post_length: "Minst antal tillåtna tecken i första inlägget (ämnestext)" min_private_message_post_length: "Minst antal tillåtna tecken i inlägg för meddelanden" min_search_term_length: "Minsta giltiga teckenlängd på sökterm" - uncategorized_description: "Beskrivningen av den okategoriserade kategorin. Lämna tomt för ingen beskrivning." allow_duplicate_topic_titles: "Tillåt ämnen med identiska rubriker." unique_posts_mins: "Hur många minuter innan en användare kan göra ett inlägg med precis samma innehåll igen" title: "Namnet på denna webbplats som används i titel-taggen." diff --git a/config/locales/server.te.yml b/config/locales/server.te.yml index b722f84565..4b8e850d32 100644 --- a/config/locales/server.te.yml +++ b/config/locales/server.te.yml @@ -492,7 +492,6 @@ te: min_topic_title_length: "శీర్షిక కు అనుమతించే అక్షరాల కనిష్ఠ పొడవు" max_topic_title_length: "శీర్షిక కు అనుమతించే అక్షరాల గరిష్ఠ పొడవు" min_search_term_length: "శోధనకు అవసరమయ్యే అక్షరాల కనీస పొడవు" - uncategorized_description: "వర్గీకరించని వర్గం యొక్క వివరణ. ఎటువంటి వివరణ లేని వాటిని ఖాళీగా వదిలివేయండి." allow_duplicate_topic_titles: "సమరూపమైన,నకిలీ శీర్షికలతో విషయాలను అనుమతించు." unique_posts_mins: "ఒక వినియోగదారు ఎన్ని నిమిషాల తర్వాత మళ్ళీ అదే విషయంతో ఒక టపా చేయవచ్చు " educate_until_posts: "వినియోగదారు వారి మొదటి (n) కొత్త టపాలు చేయడం మొదలుపెట్టినప్పుడు, కంపోజర్‌లో పాప్-అప్ కొత్త యూజర్ విద్య ప్యానెల్ చూపించు." From 13983c2f80e21873689d7c1d8eccba2367a7f744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 9 Feb 2016 23:06:08 +0100 Subject: [PATCH 046/140] rever wiki background color, just change the default color --- app/assets/stylesheets/common/base/topic-post.scss | 4 ++++ app/assets/stylesheets/common/foundation/colors.scss | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 7ac426abb8..0079a415f5 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -157,6 +157,10 @@ aside.quote { } } +.wiki .topic-body { + background-color: dark-light-diff($wiki, $secondary, 95%, -50%); +} + // this ensures consistent top margin on topic posts even if the first line of a post // is a top-margin-less element like a list or image. .topic-body .regular { diff --git a/app/assets/stylesheets/common/foundation/colors.scss b/app/assets/stylesheets/common/foundation/colors.scss index 8fe9170fc2..cdec1771e4 100644 --- a/app/assets/stylesheets/common/foundation/colors.scss +++ b/app/assets/stylesheets/common/foundation/colors.scss @@ -8,4 +8,4 @@ $highlight: #ffff4d !default; $danger: #e45735 !default; $success: #009900 !default; $love: #fa6c8d !default; -$wiki: #408040 !default; +$wiki: #ffffff !default; From 8944d62aa69eb3be11e1ef6b80216dc02e6d9edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 9 Feb 2016 23:35:40 +0100 Subject: [PATCH 047/140] add validator for the 'reply_by_email_enabled' site setting --- config/locales/server.en.yml | 2 ++ config/site_settings.yml | 4 ++- .../reply_by_email_enabled_validator.rb | 23 ++++++++++++++ .../reply_by_email_enabled_validator_spec.rb | 31 +++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 lib/validators/reply_by_email_enabled_validator.rb create mode 100644 spec/components/validators/reply_by_email_enabled_validator_spec.rb diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 0c70dfc9db..71be1163da 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1263,6 +1263,8 @@ en: pop3_polling_username_is_empty: "You must set a 'pop3 polling username' before enabling POP3 polling." pop3_polling_password_is_empty: "You must set a 'pop3 polling password' before enabling POP3 polling." pop3_polling_authentication_failed: "POP3 authentication failed. Please verify your pop3 credentials." + reply_by_email_address_is_empty: "You must set a 'reply by email address' before enabling reply by email." + pop3_polling_disabled: "You must first enabled POP3 polling before enabling reply by email." notification_types: group_mentioned: "%{group_name} was mentioned in %{link}" diff --git a/config/site_settings.yml b/config/site_settings.yml index 37176146f9..6937a8de35 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -506,7 +506,9 @@ email: client: true email_custom_headers: 'Auto-Submitted: auto-generated' email_subject: '[%{site_name}] %{optional_pm}%{optional_cat}%{topic_title}' - reply_by_email_enabled: false + reply_by_email_enabled: + default: false + validator: "ReplyByEmailEnabledValidator" reply_by_email_address: default: '' validator: "ReplyByEmailAddressValidator" diff --git a/lib/validators/reply_by_email_enabled_validator.rb b/lib/validators/reply_by_email_enabled_validator.rb new file mode 100644 index 0000000000..063dbe8229 --- /dev/null +++ b/lib/validators/reply_by_email_enabled_validator.rb @@ -0,0 +1,23 @@ +class ReplyByEmailEnabledValidator + + def initialize(opts={}) + @opts = opts + end + + def valid_value?(val) + # only validate when enabling reply by email + return true if val == "f" + # ensure reply_by_email_address is configured && polling is working + SiteSetting.reply_by_email_address.present? && + SiteSetting.pop3_polling_enabled? + end + + def error_message + if SiteSetting.reply_by_email_address.blank? + I18n.t("site_settings.errors.reply_by_email_address_is_empty") + else + I18n.t("site_settings.errors.pop3_polling_disabled") + end + end + +end diff --git a/spec/components/validators/reply_by_email_enabled_validator_spec.rb b/spec/components/validators/reply_by_email_enabled_validator_spec.rb new file mode 100644 index 0000000000..7596afef9a --- /dev/null +++ b/spec/components/validators/reply_by_email_enabled_validator_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +describe ReplyByEmailEnabledValidator do + + describe '#valid_value?' do + subject(:validator) { described_class.new } + + it "only validates when enabling the setting" do + expect(validator.valid_value?("f")).to eq(true) + end + + it "returns false if reply_by_email_address is missing" do + SiteSetting.expects(:reply_by_email_address).returns("") + expect(validator.valid_value?("t")).to eq(false) + end + + it "returns false if POP3 polling is disabled" do + SiteSetting.expects(:reply_by_email_address).returns("foo.%{reply_key}+42@bar.com") + SiteSetting.expects(:pop3_polling_enabled).returns(false) + expect(validator.valid_value?("t")).to eq(false) + end + + it "returns true when POP3 polling is enabled and the reply_by_email_address is configured" do + SiteSetting.expects(:reply_by_email_address).returns("foo.%{reply_key}+42@bar.com") + SiteSetting.expects(:pop3_polling_enabled).returns(true) + expect(validator.valid_value?("t")).to eq(true) + end + + end + +end From d0dd517f27a9cd259fa5e0da29b763abdc7a2ff9 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 10 Feb 2016 11:54:40 +1100 Subject: [PATCH 048/140] FEATURE: blank global settings should not shadow Due to https://github.com/docker/docker/issues/9298 it is a huge pain to remove ENV vars when composing images, allow us to simply treat "blank" as a ENV var that is not being shadowed. In general we always supply a value to ENV vars we are shadowing. --- lib/site_setting_extension.rb | 8 +++++--- spec/components/site_setting_extension_spec.rb | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 13392a22af..7eb33e2b8f 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -121,9 +121,11 @@ module SiteSettingExtension # exists it will be used instead of the setting and the setting will be hidden. # Useful for things like API keys on multisite. if opts[:shadowed_by_global] && GlobalSetting.respond_to?(name) - hidden_settings << name - shadowed_settings << name - current_value = GlobalSetting.send(name) + if (val = GlobalSetting.send(name)).present? + hidden_settings << name + shadowed_settings << name + current_value = val + end end if opts[:refresh] diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index 04f3622706..390c98fa26 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -447,6 +447,18 @@ describe SiteSettingExtension do end end + context "with blank global setting" do + before do + GlobalSetting.stubs(:nada).returns('') + settings.setting(:nada, 'nothing', shadowed_by_global: true) + settings.refresh! + end + + it "should return default cause nothing is set" do + expect(settings.nada).to eq('nothing') + end + end + context "with global setting" do before do GlobalSetting.stubs(:trout_api_key).returns('purringcat') From d220eb34ff0658f765ede30e68149b8166828e73 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 10 Feb 2016 16:17:41 +0800 Subject: [PATCH 049/140] Remove unused mixin. --- .../common/components/badges.css.scss | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index afe7ebd9b2..9bb9e9e97c 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -130,30 +130,6 @@ } } -@mixin cooked-badge-bullet($length, $offset:0px) { - .badge-wrapper.bullet { - span { - position: relative; - - &.badge-category-bg { - width: $length; - height: $length; - top: $offset; - } - - &.badge-category-parent-bg { - width: $length / 2; - height: $length; - top: $offset; - - & + .badge-category-bg { - width: $length / 2; - } - } - } - } -} - // Category badge dropdown // -------------------------------------------------- From abc4454e8913983b9f0774b8f0a2d18899a6089e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 10 Feb 2016 16:27:03 +0800 Subject: [PATCH 050/140] UX: Set max-width in order to trigger overflow. --- app/assets/stylesheets/common/components/badges.css.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 9bb9e9e97c..cb4f544f0a 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -25,6 +25,7 @@ &.bar { //bar category style line-height: 1.25; margin-right: 5px; + display: inline-flex; span.badge-category { color: $primary !important; @@ -130,6 +131,10 @@ } } +.autocomplete .badge-wrapper { + max-width: 230px; +} + // Category badge dropdown // -------------------------------------------------- From ee38572be37b59466710da5f87e5f6a9e571d987 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 10 Feb 2016 16:34:43 +0800 Subject: [PATCH 051/140] UX: Set max-width on category column to trigger overflow. --- app/assets/stylesheets/common/components/badges.css.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index cb4f544f0a..8438f2b858 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -131,8 +131,10 @@ } } -.autocomplete .badge-wrapper { - max-width: 230px; +.autocomplete, td.category { + .badge-wrapper { + max-width: 230px; + } } // Category badge dropdown From 39aaa181e1e948f16cdab997db983917c9a556ce Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 10 Feb 2016 17:08:57 +0800 Subject: [PATCH 052/140] FIX: Category hashtag is cooked incorrectly. --- lib/pretty_text.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index eb9f079d77..d0a3c60329 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -50,7 +50,7 @@ module PrettyText def category_hashtag_lookup(category_slug) if category = Category.query_from_hashtag_slug(category_slug) - category.url_with_id + [category.url_with_id, text] else nil end From 32cc3a66c8159bb7cf902f4cda1e23f78df38f85 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 10 Feb 2016 01:15:53 -0800 Subject: [PATCH 053/140] I can't deal with diagonal pushpins any more --- app/assets/stylesheets/common/base/_topic-list.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index 612d43c8ad..358e9f2e15 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -226,7 +226,7 @@ ol.category-breadcrumb { } .fa-thumb-tack.unpinned { - @include fa-icon-rotate(315deg, 1); + @include fa-icon-rotate(180deg, 1); color: $primary; } From c2cc9da9a784ae650d8b1ae94006943e0db9f83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 10 Feb 2016 11:18:37 +0100 Subject: [PATCH 054/140] remove the wiki color --- app/assets/stylesheets/common/base/topic-post.scss | 6 +----- app/assets/stylesheets/common/foundation/colors.scss | 1 - config/locales/client.en.yml | 4 ---- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 0079a415f5..0e2f63e5ce 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -157,10 +157,6 @@ aside.quote { } } -.wiki .topic-body { - background-color: dark-light-diff($wiki, $secondary, 95%, -50%); -} - // this ensures consistent top margin on topic posts even if the first line of a post // is a top-margin-less element like a list or image. .topic-body .regular { @@ -176,7 +172,7 @@ aside.quote { } &.wiki { cursor: pointer; - color: $wiki; + color: #408040; } &.via-email { color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); diff --git a/app/assets/stylesheets/common/foundation/colors.scss b/app/assets/stylesheets/common/foundation/colors.scss index cdec1771e4..72041265b9 100644 --- a/app/assets/stylesheets/common/foundation/colors.scss +++ b/app/assets/stylesheets/common/foundation/colors.scss @@ -8,4 +8,3 @@ $highlight: #ffff4d !default; $danger: #e45735 !default; $success: #009900 !default; $love: #fa6c8d !default; -$wiki: #ffffff !default; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 73541fbbc7..96002144e7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2225,10 +2225,6 @@ en: love: name: 'love' description: "The like button's color." - wiki: - name: 'wiki' - description: "Base color used for the background of wiki posts." - email: title: "Emails" From c7186b14036420ec66bc7136b7059d02683736a1 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 10 Feb 2016 19:47:45 +0800 Subject: [PATCH 055/140] FIX: Close autocomplete is term is blank. --- .../discourse/lib/autocomplete.js.es6 | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 71dce2d1d6..5da0fb6b02 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -242,6 +242,15 @@ export default function(options) { }); }; + const dataSource = (term, opts) => { + if (term.length !== 0 && term.trim().length === 0) { + closeAutocomplete(); + return null; + } else { + return opts.dataSource(term); + } + }; + var updateAutoComplete = function(r) { if (completeStart === null) return; @@ -304,13 +313,13 @@ export default function(options) { var prevChar = me.val().charAt(caretPosition - 1); if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) { completeStart = completeEnd = caretPosition; - updateAutoComplete(options.dataSource("")); + updateAutoComplete(dataSource("", options)); } } else if ((completeStart !== null) && (e.charCode !== 0)) { caretPosition = Discourse.Utilities.caretPosition(me[0]); term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition); term += String.fromCharCode(e.charCode); - updateAutoComplete(options.dataSource(term)); + updateAutoComplete(dataSource(term, options)); } }); @@ -360,7 +369,7 @@ export default function(options) { completeStart = c; caretPosition = completeEnd = initial; term = me[0].value.substring(c + 1, initial); - updateAutoComplete(options.dataSource(term)); + updateAutoComplete(dataSource(term, options)); return true; } } @@ -444,7 +453,7 @@ export default function(options) { closeAutocomplete(); } - updateAutoComplete(options.dataSource(term)); + updateAutoComplete(dataSource(term, options)); return true; default: completeEnd = caretPosition; From 91bb38626ce5d7023113a9b010dc8cc4c591248e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 10 Feb 2016 22:00:27 +0100 Subject: [PATCH 056/140] FEATURE: new incoming email details modal --- .../modals/admin-incoming-email.js.es6 | 17 ++++ .../admin/models/incoming-email.js.es6 | 4 + .../admin/routes/admin-email-rejected.js.es6 | 6 +- .../admin/templates/email-rejected.hbs | 2 +- .../templates/modal/admin_incoming_email.hbs | 98 +++++++++++++++++++ .../views/modals/admin-incoming-email.js.es6 | 7 ++ .../stylesheets/common/admin/admin_base.scss | 34 +++++++ app/controllers/admin/email_controller.rb | 7 ++ .../incoming_email_details_serializer.rb | 82 ++++++++++++++++ config/locales/client.en.yml | 13 +++ config/locales/server.en.yml | 12 +++ config/routes.rb | 1 + lib/email/receiver.rb | 12 +-- 13 files changed, 285 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/admin/controllers/modals/admin-incoming-email.js.es6 create mode 100644 app/assets/javascripts/admin/templates/modal/admin_incoming_email.hbs create mode 100644 app/assets/javascripts/admin/views/modals/admin-incoming-email.js.es6 create mode 100644 app/serializers/incoming_email_details_serializer.rb diff --git a/app/assets/javascripts/admin/controllers/modals/admin-incoming-email.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-incoming-email.js.es6 new file mode 100644 index 0000000000..bc9cd2edd8 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-incoming-email.js.es6 @@ -0,0 +1,17 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import IncomingEmail from 'admin/models/incoming-email'; +import computed from 'ember-addons/ember-computed-decorators'; +import { longDate } from 'discourse/lib/formatter'; + +export default Ember.Controller.extend(ModalFunctionality, { + + @computed("model.date") + date(d) { + return longDate(d); + }, + + load(id) { + return IncomingEmail.find(id).then(result => this.set("model", result)); + } + +}); diff --git a/app/assets/javascripts/admin/models/incoming-email.js.es6 b/app/assets/javascripts/admin/models/incoming-email.js.es6 index d0e5b43c59..82534c5c43 100644 --- a/app/assets/javascripts/admin/models/incoming-email.js.es6 +++ b/app/assets/javascripts/admin/models/incoming-email.js.es6 @@ -14,6 +14,10 @@ IncomingEmail.reopenClass({ return this._super(attrs); }, + find(id) { + return Discourse.ajax(`/admin/email/incoming/${id}.json`); + }, + findAll(filter, offset) { filter = filter || {}; offset = offset || 0; diff --git a/app/assets/javascripts/admin/routes/admin-email-rejected.js.es6 b/app/assets/javascripts/admin/routes/admin-email-rejected.js.es6 index 4d96868d29..a7819b31a6 100644 --- a/app/assets/javascripts/admin/routes/admin-email-rejected.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-email-rejected.js.es6 @@ -5,9 +5,9 @@ export default AdminEmailIncomings.extend({ status: "rejected", actions: { - showRawEmail(incomingEmailId) { - showModal('raw-email'); - this.controllerFor('raw_email').loadIncomingRawEmail(incomingEmailId); + showIncomingEmail(id) { + showModal('modals/admin-incoming-email'); + this.controllerFor("modals/admin-incoming-email").load(id); } } diff --git a/app/assets/javascripts/admin/templates/email-rejected.hbs b/app/assets/javascripts/admin/templates/email-rejected.hbs index ea5c22112d..3c5f3cba8e 100644 --- a/app/assets/javascripts/admin/templates/email-rejected.hbs +++ b/app/assets/javascripts/admin/templates/email-rejected.hbs @@ -42,7 +42,7 @@
{{else}} diff --git a/app/assets/javascripts/admin/templates/modal/admin_incoming_email.hbs b/app/assets/javascripts/admin/templates/modal/admin_incoming_email.hbs new file mode 100644 index 0000000000..d4907a7e32 --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin_incoming_email.hbs @@ -0,0 +1,98 @@ +
+ +
+

{{model.error}}

+ {{#if model.error_description}} +

{{model.error_description}}

+ {{/if}} +
+
+ +
+ +{{#if model.return_path}} +
+ +
+ {{model.return_path}} +
+
+{{/if}} + +
+ +
+ {{model.message_id}} +
+
+ +{{#if model.references}} +
+ +
+
    + {{#each reference in model.references}} +
  • {{reference}}
  • + {{/each}} +
+
+
+{{/if}} + +{{#if model.in_reply_to}} +
+ +
+ {{model.in_reply_to}} +
+
+{{/if}} + +
+ +
+ {{date}} +
+
+ +
+ +
+ {{model.from}} +
+
+ +
+ +
+
    + {{#each to in model.to}} +
  • {{to}}
  • + {{/each}} +
+
+
+ +{{#if model.cc}} +
+ +
+ {{model.cc}} +
+
+{{/if}} + +
+ +
+ {{model.subject}} +
+
+ +
+ +
+ {{textarea value=model.body}} +
+
+ diff --git a/app/assets/javascripts/admin/views/modals/admin-incoming-email.js.es6 b/app/assets/javascripts/admin/views/modals/admin-incoming-email.js.es6 new file mode 100644 index 0000000000..576feff433 --- /dev/null +++ b/app/assets/javascripts/admin/views/modals/admin-incoming-email.js.es6 @@ -0,0 +1,7 @@ +import ModalBodyView from "discourse/views/modal-body"; + +export default ModalBodyView.extend({ + templateName: 'admin/templates/modal/admin_incoming_email', + classNames: ['incoming-emails'], + title: I18n.t('admin.email.incoming_emails.modal.title') +}); diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 5a5df3b738..90c8bf41c7 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1798,6 +1798,40 @@ table#user-badges { } } +.incoming-emails { + .control-group { + margin: 8px 0; + } + .controls { + margin-left: 110px; + } + p { + margin: 5px 10px; + } + .error-description { + color: #919191; + font-size: 90%; + } + hr { + margin: 0; + } + label { + font-weight: bold; + float: left; + width: 100px; + text-align: right; + margin: 0 10px; + } + ul { + list-style: none; + margin: 0 10px; + } + textarea { + width: 95%; + height: 150px; + } +} + // Mobile specific styles // Mobile view text-inputs need some padding .mobile-view .admin-contents { diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index c8c603e9fd..4b685c48e2 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -57,6 +57,13 @@ class Admin::EmailController < Admin::AdminController render json: { raw_email: incoming_email.raw } end + def incoming + params.require(:id) + incoming_email = IncomingEmail.find(params[:id].to_i) + serializer = IncomingEmailDetailsSerializer.new(incoming_email, root: false) + render_json_dump(serializer) + end + private def filter_email_logs(email_logs, params) diff --git a/app/serializers/incoming_email_details_serializer.rb b/app/serializers/incoming_email_details_serializer.rb new file mode 100644 index 0000000000..ac9e346caa --- /dev/null +++ b/app/serializers/incoming_email_details_serializer.rb @@ -0,0 +1,82 @@ +class IncomingEmailDetailsSerializer < ApplicationSerializer + + attributes :error, + :error_description, + :return_path, + :date, + :from, + :to, + :cc, + :message_id, + :references, + :in_reply_to, + :subject, + :body + + def initialize(incoming_email, opts) + super + @error_string = incoming_email.error + @mail = Mail.new(incoming_email.raw) + end + + EMAIL_RECEIVER_ERROR_PREFIX = "Email::Receiver::".freeze + + def error + @error_string + end + + def error_description + error_name = @error_string.sub(EMAIL_RECEIVER_ERROR_PREFIX, "").underscore + I18n.t("emails.incoming.errors.#{error_name}") + end + + def include_error_description? + @error_string[EMAIL_RECEIVER_ERROR_PREFIX] + end + + def return_path + @mail.return_path + end + + def date + @mail.date + end + + def from + @mail.from.first.downcase + end + + def to + @mail.to.map(&:downcase) + end + + def cc + @mail.cc.map(&:downcase) if @mail.cc.present? + end + + def message_id + @mail.message_id + end + + def references + references = Email::Receiver.extract_references(@mail.references) + references.delete(@mail.in_reply_to) if references + references + end + + def in_reply_to + @mail.in_reply_to + end + + def subject + @mail.subject.presence || "(no subject)" + end + + def body + body = @mail.text_part.decoded rescue nil + body ||= @mail.html_part.decoded rescue nil + body ||= @mail.body.decoded rescue nil + body.strip.truncate_words(100, escape: false) + end + +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 96002144e7..acd880b1bc 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2262,6 +2262,19 @@ en: subject: "Subject" error: "Error" none: "No incoming emails found." + modal: + title: "Incoming Email Details" + error: "Error" + return_path: "Return-Path" + message_id: "Message-Id" + in_reply_to: "In-Reply-To" + references: "References" + date: "Date" + from: "From" + to: "To" + cc: "Cc" + subject: "Subject" + body: "Body" filters: from_placeholder: "from@example.com" to_placeholder: "to@example.com" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 71be1163da..3cb10c0f61 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -50,6 +50,18 @@ en: emails: incoming: default_subject: "Incoming email from %{email}" + errors: + empty_email_error: "Happens when the raw mail we received was blank." + no_message_id_error: "Happens when the mail has no 'Message-Id' header." + auto_generated_email_error: "Happens when the 'precedence' header is set to: list, junk, bulk or auto_reply, or when any other header contains: auto-submitted, auto-replied or auto-generated." + no_body_detected_error: "Happens when we couldn't extract a body and there was no attachments." + inactive_user_error: "Happens when the sender is not active." + bad_destination_address: "Happens when none of the email addresses in To/Cc/Bcc fields matched a configured incoming email address." + strangers_not_allowed_error: "Happens when a user tried to create a new topic in a category they're not a member of." + insufficient_trust_level_error: "Happens when a use tried to create a new topic in a category they don't have the required trust level for." + reply_user_not_matching_error: "Happens when a reply came in from a different email address the notification was sent to." + topic_not_found_error: "Happens when a reply came in but the related topic has been deleted." + topic_closed_error: "Happens when a reply came in but the related topic has been closed." errors: &errors format: ! '%{attribute} %{message}' diff --git a/config/routes.rb b/config/routes.rb index d0457c90c4..8b77b6fc91 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -125,6 +125,7 @@ Discourse::Application.routes.draw do get "received" get "rejected" get "/incoming/:id/raw" => "email#raw_email" + get "/incoming/:id" => "email#incoming" get "preview-digest" => "email#preview_digest" post "handle_mail" end diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 049f7fcff7..4a5b5462d6 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -215,7 +215,7 @@ module Email end def find_related_post - message_ids = [@mail.in_reply_to, extract_references] + message_ids = [@mail.in_reply_to, Email::Receiver.extract_references(@mail.references)] message_ids.flatten! message_ids.select!(&:present?) message_ids.uniq! @@ -226,11 +226,11 @@ module Email .first end - def extract_references - if Array === @mail.references - @mail.references - elsif @mail.references.present? - @mail.references.split(/[\s,]/).map { |r| r.sub(/^$/, "") } + def self.extract_references(references) + if Array === references + references + elsif references.present? + references.split(/[\s,]/).map { |r| r.sub(/^$/, "") } end end From cc695605b720f6ff01a600d9d98a36d346b528b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 10 Feb 2016 22:43:00 +0100 Subject: [PATCH 057/140] fix textarea styling in incoming email details modal --- app/assets/stylesheets/common/admin/admin_base.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 90c8bf41c7..15e1ff697b 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1829,6 +1829,8 @@ table#user-badges { textarea { width: 95%; height: 150px; + font-family: monospace; + box-shadow: none; } } From 26763c141c762a79816e5f3a925103a06de67a68 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 10 Feb 2016 15:26:23 -0800 Subject: [PATCH 058/140] rotated thumbtack glyph reverses left/right padding --- app/assets/stylesheets/common/base/_topic-list.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index 358e9f2e15..e23ff917ff 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -228,6 +228,9 @@ ol.category-breadcrumb { .fa-thumb-tack.unpinned { @include fa-icon-rotate(180deg, 1); color: $primary; + /* because it is rotated, right becomes left! */ + padding-left: 3px; + padding-right: 0 !important; } .topic-statuses .fa { From 081c196b521648b3997d8af2db868790767337d4 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 10 Feb 2016 16:34:37 -0800 Subject: [PATCH 059/140] minor copyedit --- config/locales/client.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index acd880b1bc..f4752da7b6 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1113,8 +1113,8 @@ en: top: "There are no top topics." search: "There are no search results." educate: - new: '

Your new topics appear here.

By default, topics are considered new and will show a new indicator if they were created in the last 2 days.

You can change this in your preferences.

' - unread: '

Your unread topics appear here.

By default, topics are considered unread and will show unread counts 1 if you:

  • Created the topic
  • Replied to the topic
  • Read the topic for more than 4 minutes

Or if you have explicitly set the topic to Tracked or Watched via the notification control at the bottom of each topic.

You can change this in your preferences.

' + new: '

Your new topics appear here.

By default, topics are considered new and will show a new indicator if they were created in the last 2 days.

Visit your preferences to change this.

' + unread: '

Your unread topics appear here.

By default, topics are considered unread and will show unread counts 1 if you:

  • Created the topic
  • Replied to the topic
  • Read the topic for more than 4 minutes

Or if you have explicitly set the topic to Tracked or Watched via the notification control at the bottom of each topic.

Visit your preferences to change this.

' bottom: latest: "There are no more latest topics." hot: "There are no more hot topics." From 5120dcfb3d9682743e93927a5a90ab6dc1aab8b8 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 10 Feb 2016 16:36:25 -0800 Subject: [PATCH 060/140] we don't need to show (currently enabled) --- config/locales/client.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f4752da7b6..97dbc78ac3 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -474,9 +474,9 @@ en: perm_denied_btn: "Permission Denied" perm_denied_expl: "You denied permission for notifications. Allow notifications via your browser settings." disable: "Disable Notifications" - currently_enabled: "(currently enabled)" + currently_enabled: "" enable: "Enable Notifications" - currently_disabled: "(currently disabled)" + currently_disabled: "" each_browser_note: "Note: You have to change this setting on every browser you use." dismiss_notifications: "Mark all as Read" dismiss_notifications_tooltip: "Mark all unread notifications as read" From cad7fc10620ac8daaf4e563438bc6574f63f5784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 11 Feb 2016 10:39:57 +0100 Subject: [PATCH 061/140] FIX: don't allow blocked user to send emails in --- app/jobs/scheduled/poll_mailbox.rb | 1 + config/locales/server.en.yml | 8 ++++++++ lib/email/receiver.rb | 6 ++++-- spec/components/email/receiver_spec.rb | 5 +++++ spec/fixtures/emails/blocked_sender.eml | 9 +++++++++ spec/fixtures/emails/staged_sender.eml | 2 +- 6 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 spec/fixtures/emails/blocked_sender.eml diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb index c353267852..01ac885b44 100644 --- a/app/jobs/scheduled/poll_mailbox.rb +++ b/app/jobs/scheduled/poll_mailbox.rb @@ -38,6 +38,7 @@ module Jobs when Email::Receiver::NoMessageIdError then :email_reject_no_message_id when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated when Email::Receiver::InactiveUserError then :email_reject_inactive_user + when Email::Receiver::BlockedUserError then :email_reject_blocked_user when Email::Receiver::BadDestinationAddress then :email_reject_bad_destination_address when Email::Receiver::StrangersNotAllowedError then :email_reject_strangers_not_allowed when Email::Receiver::InsufficientTrustLevelError then :email_reject_insufficient_trust_level diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 3cb10c0f61..f0f4c26cd4 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -56,6 +56,7 @@ en: auto_generated_email_error: "Happens when the 'precedence' header is set to: list, junk, bulk or auto_reply, or when any other header contains: auto-submitted, auto-replied or auto-generated." no_body_detected_error: "Happens when we couldn't extract a body and there was no attachments." inactive_user_error: "Happens when the sender is not active." + blocked_user_error: "Happens when the sender has been blocked." bad_destination_address: "Happens when none of the email addresses in To/Cc/Bcc fields matched a configured incoming email address." strangers_not_allowed_error: "Happens when a user tried to create a new topic in a category they're not a member of." insufficient_trust_level_error: "Happens when a use tried to create a new topic in a category they don't have the required trust level for." @@ -1795,6 +1796,13 @@ en: Your account associated with this email address is not activated. Please activate your account before sending emails in. + email_reject_blocked_user: + subject_template: "[%{site_name}] Email issue -- Blocked User" + text_body_template: | + We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. + + Your account associated with this email address has been blocked. + email_reject_reply_user_not_matching: subject_template: "[%{site_name}] Email issue -- Reply User Not Matching" text_body_template: | diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 4a5b5462d6..fdaaddea35 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -13,6 +13,7 @@ module Email class AutoGeneratedEmailError < ProcessingError; end class NoBodyDetectedError < ProcessingError; end class InactiveUserError < ProcessingError; end + class BlockedUserError < ProcessingError; end class BadDestinationAddress < ProcessingError; end class StrangersNotAllowedError < ProcessingError; end class InsufficientTrustLevelError < ProcessingError; end @@ -53,8 +54,9 @@ module Email body = select_body || "" raise AutoGeneratedEmailError if is_auto_generated? - raise NoBodyDetectedError if body.blank? && !@mail.has_attachments? - raise InactiveUserError if !user.active && !user.staged + raise NoBodyDetectedError if body.blank? && !@mail.has_attachments? + raise InactiveUserError if !user.active && !user.staged + raise BlockedUserError if user.blocked if action = subscription_action_for(body, subject) message = SubscriptionMailer.send(action, user) diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 36121204c2..236c372046 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -43,6 +43,11 @@ describe Email::Receiver do expect { process(:inactive_sender) }.to raise_error(Email::Receiver::InactiveUserError) end + it "raises a BlockedUserError when the sender has been blocked" do + Fabricate(:user, email: "blocked@bar.com", blocked: true) + expect { process(:blocked_sender) }.to raise_error(Email::Receiver::BlockedUserError) + end + skip "doesn't raise an InactiveUserError when the sender is staged" do Fabricate(:user, email: "staged@bar.com", active: false, staged: true) expect { process(:staged_sender) }.not_to raise_error diff --git a/spec/fixtures/emails/blocked_sender.eml b/spec/fixtures/emails/blocked_sender.eml new file mode 100644 index 0000000000..19bab36237 --- /dev/null +++ b/spec/fixtures/emails/blocked_sender.eml @@ -0,0 +1,9 @@ +Return-Path: +From: Foo Bar +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <8@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. diff --git a/spec/fixtures/emails/staged_sender.eml b/spec/fixtures/emails/staged_sender.eml index 6ee856614e..e7828e9146 100644 --- a/spec/fixtures/emails/staged_sender.eml +++ b/spec/fixtures/emails/staged_sender.eml @@ -1,7 +1,7 @@ Return-Path: From: Foo Bar Date: Fri, 15 Jan 2016 00:12:43 +0100 -Message-ID: <9@foo.bar.mail> +Message-ID: <39@foo.bar.mail> Mime-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: 7bit From f2c64a35805713ab42dc6dd95f7041d38011fe20 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 11 Feb 2016 11:16:09 +0800 Subject: [PATCH 062/140] FIX: Client settings were not being published. --- lib/site_setting_extension.rb | 2 +- spec/components/site_setting_extension_spec.rb | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 7eb33e2b8f..a6a46d6390 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -151,7 +151,7 @@ module SiteSettingExtension # just like a setting, except that it is available in javascript via DiscourseSession def client_setting(name, default = nil, opts = {}) setting(name, default, opts) - client_settings << name + client_settings << name.to_sym end def settings_hash diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index 390c98fa26..8d9fd2eb82 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -19,7 +19,7 @@ describe SiteSettingExtension do end end end - + let :provider_local do SiteSettings::LocalProcessProvider.new end @@ -140,6 +140,17 @@ describe SiteSettingExtension do settings.set("test_setting", 12) expect(settings.test_setting).to eq(12) end + + it "should publish changes to clients" do + settings.setting("test_setting", 100) + settings.client_setting("test_setting") + + messages = MessageBus.track_publish do + settings.test_setting = 88 + end + + expect(messages.map(&:channel).include?('/client_settings')).to eq(true) + end end end From 3bf931ce5481963f3eff81c91e263894f15bb42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 11 Feb 2016 16:04:40 +0100 Subject: [PATCH 063/140] FIX: should have been 'category_slug' --- lib/pretty_text.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index d0a3c60329..fbd30ba232 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -50,7 +50,7 @@ module PrettyText def category_hashtag_lookup(category_slug) if category = Category.query_from_hashtag_slug(category_slug) - [category.url_with_id, text] + [category.url_with_id, category_slug] else nil end From 86819f08c3294313b9b414f3c38644e4a60284c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 11 Feb 2016 18:48:09 +0100 Subject: [PATCH 064/140] FIX: use RFC-compliant previous replies separator --- app/mailers/user_notifications.rb | 2 +- lib/email/receiver.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 9029341d90..6f2345137b 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -275,7 +275,7 @@ class UserNotifications < ActionMailer::Base context_posts = context_posts.to_a if context_posts.present? - context << "---\n*#{I18n.t('user_notifications.previous_discussion')}*\n" + context << "-- \n*#{I18n.t('user_notifications.previous_discussion')}*\n" context_posts.each do |cp| context << email_post_markdown(cp) end diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index fdaaddea35..cb45b7d8ba 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -143,7 +143,7 @@ module Email end def previous_replies_regex - @previous_replies_regex ||= /^---\n\*#{I18n.t("user_notifications.previous_discussion")}\*\n/im + @previous_replies_regex ||= /^--[- ]\n\*#{I18n.t("user_notifications.previous_discussion")}\*\n/im end def trim_discourse_markers(reply) From 3e872502150d697b0a35c27486aa67adbe88a504 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Feb 2016 17:51:08 +1100 Subject: [PATCH 065/140] UX: initial take at collapsing mobile nav on user page --- .../discourse/components/mobile-nav.js.es6 | 38 +++++++++++ .../controllers/user-activity.js.es6 | 2 +- .../controllers/user-notifications.js.es6 | 2 + .../controllers/user-private-messages.js.es6 | 6 +- .../discourse/controllers/user.js.es6 | 3 +- .../templates/components/mobile-nav.hbs | 1 + .../mobile/components/mobile-nav.hbs | 4 ++ .../discourse/templates/user/activity.hbs | 9 +-- .../discourse/templates/user/messages.hbs | 5 +- .../templates/user/notifications.hbs | 4 +- .../discourse/templates/user/user.hbs | 7 +- app/assets/stylesheets/mobile/discourse.scss | 66 +++++++++++++++++++ 12 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/mobile-nav.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/mobile-nav.hbs create mode 100644 app/assets/javascripts/discourse/templates/mobile/components/mobile-nav.hbs diff --git a/app/assets/javascripts/discourse/components/mobile-nav.js.es6 b/app/assets/javascripts/discourse/components/mobile-nav.js.es6 new file mode 100644 index 0000000000..939e3010fa --- /dev/null +++ b/app/assets/javascripts/discourse/components/mobile-nav.js.es6 @@ -0,0 +1,38 @@ +export default Ember.Component.extend({ + + _init: function(){ + if (!this.get('site.mobileView')) { + var classes = this.get('desktopClass'); + if (classes) { + classes = classes.split(' '); + this.set('classNames', classes); + } + } + }.on('init'), + + tagName: 'ul', + + classNames: ['mobile-nav'], + + currentPathChanged: function(){ + this.set('expanded', false); + Em.run.next(() => this._updateSelectedHtml()); + }.observes('currentPath'), + + _updateSelectedHtml(){ + const active = this.$('.active'); + if (active && active.html) { + this.set('selectedHtml', active.html()); + } + }, + + didInsertElement(){ + this._updateSelectedHtml(); + }, + + actions: { + toggleExpanded(){ + this.toggleProperty('expanded'); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 index 59c6b6dd81..55ab7846ca 100644 --- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 @@ -3,7 +3,7 @@ import { exportUserArchive } from 'discourse/lib/export-csv'; export default Ember.Controller.extend({ userActionType: null, needs: ["application", "user"], - + currentPath: Em.computed.alias('controllers.application.currentPath'), viewingSelf: Em.computed.alias("controllers.user.viewingSelf"), showBookmarks: Em.computed.alias("controllers.user.showBookmarks"), diff --git a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 index 85865c16e5..c06137a9e1 100644 --- a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 @@ -7,6 +7,8 @@ export default Ember.ArrayController.extend({ showDismissButton: Ember.computed.gt('user.total_unread_notifications', 0), + currentPath: Em.computed.alias('controllers.application.currentPath'), + actions: { resetNew: function() { Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { diff --git a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 index eb8d13c721..73c8ff293a 100644 --- a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 @@ -3,11 +3,11 @@ import Topic from 'discourse/models/topic'; export default Ember.Controller.extend({ - needs: ["user-topics-list", "user"], + needs: ["application", "user-topics-list", "user"], pmView: false, - viewingSelf: Em.computed.alias("controllers.user.viewingSelf"), + viewingSelf: Em.computed.alias('controllers.user.viewingSelf'), isGroup: Em.computed.equal('pmView', 'groups'), - + currentPath: Em.computed.alias('controllers.application.currentPath'), selected: Em.computed.alias('controllers.user-topics-list.selected'), bulkSelectEnabled: Em.computed.alias('controllers.user-topics-list.bulkSelectEnabled'), diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 64202d0985..24b26957da 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -6,7 +6,8 @@ import User from 'discourse/models/user'; export default Ember.Controller.extend(CanCheckEmails, { indexStream: false, userActionType: null, - needs: ['user-notifications', 'user-topics-list'], + needs: ['application','user-notifications', 'user-topics-list'], + currentPath: Em.computed.alias('controllers.application.currentPath'), @computed("content.username") viewingSelf(username) { diff --git a/app/assets/javascripts/discourse/templates/components/mobile-nav.hbs b/app/assets/javascripts/discourse/templates/components/mobile-nav.hbs new file mode 100644 index 0000000000..889d9eeadc --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/mobile-nav.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/app/assets/javascripts/discourse/templates/mobile/components/mobile-nav.hbs b/app/assets/javascripts/discourse/templates/mobile/components/mobile-nav.hbs new file mode 100644 index 0000000000..6b46d500ee --- /dev/null +++ b/app/assets/javascripts/discourse/templates/mobile/components/mobile-nav.hbs @@ -0,0 +1,4 @@ +
  • {{{selectedHtml}}}
  • +
      +{{yield}} +
    diff --git a/app/assets/javascripts/discourse/templates/user/activity.hbs b/app/assets/javascripts/discourse/templates/user/activity.hbs index e94b740c9c..4eb0971cc1 100644 --- a/app/assets/javascripts/discourse/templates/user/activity.hbs +++ b/app/assets/javascripts/discourse/templates/user/activity.hbs @@ -1,26 +1,21 @@
    - + {{/mobile-nav}} {{#if viewingSelf}}
    diff --git a/app/assets/javascripts/discourse/templates/user/messages.hbs b/app/assets/javascripts/discourse/templates/user/messages.hbs index 9b02cc4f23..afc7d0f427 100644 --- a/app/assets/javascripts/discourse/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/templates/user/messages.hbs @@ -4,7 +4,8 @@ {{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}} {{/if}} {{/unless}} - + {{/mobile-nav}}
    diff --git a/app/assets/javascripts/discourse/templates/user/notifications.hbs b/app/assets/javascripts/discourse/templates/user/notifications.hbs index aece3ca5ca..385e029b6b 100644 --- a/app/assets/javascripts/discourse/templates/user/notifications.hbs +++ b/app/assets/javascripts/discourse/templates/user/notifications.hbs @@ -1,6 +1,6 @@
    - + {{/mobile-nav}}
    diff --git a/app/assets/javascripts/discourse/templates/user/user.hbs b/app/assets/javascripts/discourse/templates/user/user.hbs index 6d9ac7af34..3971824739 100644 --- a/app/assets/javascripts/discourse/templates/user/user.hbs +++ b/app/assets/javascripts/discourse/templates/user/user.hbs @@ -148,13 +148,12 @@ {{/unless}}
    - + {{/mobile-nav}}
    diff --git a/app/assets/stylesheets/mobile/discourse.scss b/app/assets/stylesheets/mobile/discourse.scss index a023c2e7ef..acc5ab256c 100644 --- a/app/assets/stylesheets/mobile/discourse.scss +++ b/app/assets/stylesheets/mobile/discourse.scss @@ -93,3 +93,69 @@ h2#site-text-logo .badge-wrapper { font-weight: normal; } + +.user-table { + position: relative; +} + +.mobile-view .mobile-nav { + &.messages-nav, &.notifications-nav, &.activity-nav { + position: absolute; + right: 0px; + top: -55px; + } +} + + +.mobile-view .mobile-nav { + a .fa { + margin-right: 8px; + } + a { + color: $primary; + } + margin: 0; + padding: 0; + background: dark-light-diff($primary, $secondary, 90%, -65%); + list-style: none; + overflow: visible; + position: relative; + width: 40%; + + > li > a { + padding: 8px 10px; + height: 100%; + width: 100%; + display: block; + } + .fa-caret-down { + position: absolute; + right: 0px; + } + + .drop { + display: none; + } + .drop.expanded { + left: 0; + display: block; + position: absolute; + z-index: 10000000; + background-color: $secondary; + width: 98%; + list-style: none; + margin: 0; + padding: 5px; + border: 1px solid #eaeaea; + li { + margin: 5px 0; + padding: 0; + a { + width: 100%; + height: 100%; + display: block; + padding: 5px 8px; + } + } + } +} From b0d2e69cc39e35b76d946258f2de95760ee544a3 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 12 Feb 2016 15:18:34 +0800 Subject: [PATCH 066/140] FIX: Update log level to warn. --- .../connection_adapters/postgresql_fallback_adapter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index b1f88e372d..980dfafa0f 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -22,12 +22,12 @@ class PostgreSQLFallbackHandler Thread.new do begin - logger.info "#{self.class}: Checking master server..." + logger.warn "#{self.class}: Checking master server..." connection = ActiveRecord::Base.postgresql_connection(config) if connection.active? connection.disconnect! - logger.info "#{self.class}: Master server is active. Reconnecting..." + logger.warn "#{self.class}: Master server is active. Reconnecting..." ActiveRecord::Base.establish_connection(config) Discourse.disable_readonly_mode @master = true From 6478f5defafd1e54c265e29f8a18aefebfbb5611 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Feb 2016 18:20:22 +1100 Subject: [PATCH 067/140] UX: envelope glyph for suggested PMs --- app/assets/javascripts/discourse/templates/topic.hbs | 2 +- app/assets/javascripts/discourse/views/topic.js.es6 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 93358cf1b2..a0b211efd8 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -110,7 +110,7 @@ {{#if model.details.suggested_topics.length}}
    -

    {{unbound view.suggestedTitle}}

    +

    {{{unbound view.suggestedTitle}}}

    {{#if model.isPrivateMessage}} {{basic-topic-list diff --git a/app/assets/javascripts/discourse/views/topic.js.es6 b/app/assets/javascripts/discourse/views/topic.js.es6 index 89340de6ca..58939896bd 100644 --- a/app/assets/javascripts/discourse/views/topic.js.es6 +++ b/app/assets/javascripts/discourse/views/topic.js.es6 @@ -167,7 +167,7 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli suggestedTitle: function(){ return this.get('controller.model.isPrivateMessage') ? - I18n.t("suggested_topics.pm_title") : + " " + I18n.t("suggested_topics.pm_title") : I18n.t("suggested_topics.title"); }.property() }); From f77dfda097487737cd7497d867eb2d572135665d Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Feb 2016 22:06:14 +1100 Subject: [PATCH 068/140] FIX: bind the suggested topic/messages text --- app/assets/javascripts/discourse/templates/topic.hbs | 2 +- app/assets/javascripts/discourse/views/topic.js.es6 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index a0b211efd8..4bb3b7ebd7 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -110,7 +110,7 @@ {{#if model.details.suggested_topics.length}}
    -

    {{{unbound view.suggestedTitle}}}

    +

    {{{view.suggestedTitle}}}

    {{#if model.isPrivateMessage}} {{basic-topic-list diff --git a/app/assets/javascripts/discourse/views/topic.js.es6 b/app/assets/javascripts/discourse/views/topic.js.es6 index 58939896bd..93b3adb129 100644 --- a/app/assets/javascripts/discourse/views/topic.js.es6 +++ b/app/assets/javascripts/discourse/views/topic.js.es6 @@ -169,7 +169,7 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli return this.get('controller.model.isPrivateMessage') ? " " + I18n.t("suggested_topics.pm_title") : I18n.t("suggested_topics.title"); - }.property() + }.property('topic') }); function highlight(postNumber) { From c740b423286871eab7c17ca24917b58f379d055f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 12 Feb 2016 12:10:30 +0100 Subject: [PATCH 069/140] FIX: whitelist post_types used in context in email notifications --- app/mailers/user_notifications.rb | 5 ++++- spec/mailers/user_notifications_spec.rb | 27 ++++++++++++------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 6f2345137b..5172942781 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -186,11 +186,14 @@ class UserNotifications < ActionMailer::Base end def self.get_context_posts(post, topic_user) + allowed_post_types = [Post.types[:regular]] + allowed_post_types << Post.types[:whisper] if topic_user.try(:user).try(:staff?) + context_posts = Post.where(topic_id: post.topic_id) .where("post_number < ?", post.post_number) .where(user_deleted: false) .where(hidden: false) - .where(post_type: Topic.visible_post_types) + .where(post_type: allowed_post_types) .order('created_at desc') .limit(SiteSetting.email_posts_context) diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index 3934849544..947dd9a466 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -6,21 +6,20 @@ describe UserNotifications do describe "#get_context_posts" do it "does not include hidden/deleted/user_deleted posts in context" do - post = create_post - reply1 = create_post(topic: post.topic) - reply2 = create_post(topic: post.topic) - reply3 = create_post(topic: post.topic) - reply4 = create_post(topic: post.topic) + post1 = create_post + post2 = Fabricate(:post, topic: post1.topic, deleted_at: 1.day.ago) + post3 = Fabricate(:post, topic: post1.topic, user_deleted: true) + post4 = Fabricate(:post, topic: post1.topic, hidden: true) + post5 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:moderator_action]) + post6 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:small_action]) + post7 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:whisper]) + last = Fabricate(:post, topic: post1.topic) - reply1.trash! - - reply2.user_deleted = true - reply2.save - - reply3.hidden = true - reply3.save - - expect(UserNotifications.get_context_posts(reply4, nil).count).to eq(1) + # default is only post #1 + expect(UserNotifications.get_context_posts(last, nil).count).to eq(1) + # staff members can also see the whisper + tu = TopicUser.new(topic: post1.topic, user: build(:moderator)) + expect(UserNotifications.get_context_posts(last, tu).count).to eq(2) end end From 6fc2d9db3a3c48ee8379f4c5061ecebada96729c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Sat, 13 Feb 2016 00:02:23 +0800 Subject: [PATCH 070/140] UX: Fix a bunch of overflowing links on mobile nav. --- app/assets/stylesheets/mobile/discourse.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/mobile/discourse.scss b/app/assets/stylesheets/mobile/discourse.scss index acc5ab256c..d8b34c96ab 100644 --- a/app/assets/stylesheets/mobile/discourse.scss +++ b/app/assets/stylesheets/mobile/discourse.scss @@ -126,6 +126,7 @@ h2#site-text-logo padding: 8px 10px; height: 100%; width: 100%; + box-sizing: border-box; display: block; } .fa-caret-down { @@ -142,16 +143,16 @@ h2#site-text-logo position: absolute; z-index: 10000000; background-color: $secondary; - width: 98%; + width: 100%; list-style: none; margin: 0; padding: 5px; border: 1px solid #eaeaea; + box-sizing: border-box; li { margin: 5px 0; padding: 0; a { - width: 100%; height: 100%; display: block; padding: 5px 8px; From 06c9e799844a622971b2af4874d722a20896687c Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 12 Feb 2016 14:33:13 -0500 Subject: [PATCH 071/140] FIX: pending flags reminder email was ignoring the 'notify about flags after' site setting. --- app/jobs/scheduled/pending_flags_reminder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/scheduled/pending_flags_reminder.rb b/app/jobs/scheduled/pending_flags_reminder.rb index d4a68e6f14..8b196d4ae6 100644 --- a/app/jobs/scheduled/pending_flags_reminder.rb +++ b/app/jobs/scheduled/pending_flags_reminder.rb @@ -9,7 +9,7 @@ module Jobs def execute(args) if SiteSetting.notify_about_flags_after > 0 && PostAction.flagged_posts_count > 0 && - FlagQuery.flagged_post_actions('active').where('post_actions.created_at < ?', 48.hours.ago).pluck(:id).count > 0 + FlagQuery.flagged_post_actions('active').where('post_actions.created_at < ?', SiteSetting.notify_about_flags_after.to_i.hours.ago).pluck(:id).count > 0 message = PendingFlagsMailer.notify Email::Sender.new(message, :pending_flags_reminder).send From 3939b9ec7d2bd4ee3f521878a8f013ba350709b5 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 12 Feb 2016 17:20:38 -0500 Subject: [PATCH 072/140] FIX: restore in development mode connects to the wrong database --- lib/backup_restore/backup_restore.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/backup_restore/backup_restore.rb b/lib/backup_restore/backup_restore.rb index e0f02ecb1b..cde7cfefd4 100644 --- a/lib/backup_restore/backup_restore.rb +++ b/lib/backup_restore/backup_restore.rb @@ -100,7 +100,7 @@ module BackupRestore DatabaseConfiguration = Struct.new(:host, :port, :username, :password, :database) def self.database_configuration - config = Rails.env.production? ? ActiveRecord::Base.connection_pool.spec.config : Rails.configuration.database_configuration[Rails.env] + config = ActiveRecord::Base.connection_pool.spec.config config = config.with_indifferent_access DatabaseConfiguration.new( From 48cb386d58d7e5b2d31b8377ff488271b05d4bf8 Mon Sep 17 00:00:00 2001 From: Erik Bernhardson Date: Fri, 12 Feb 2016 22:02:24 -0800 Subject: [PATCH 073/140] Take filename to write to as optional parameter to export_category My discourse instance will be making regular automated public backups of specific categories. It's preferred to be able to directly control the path and filename of the output, rather than letting discourse choose for me. This was already mostly supported, a filename parameter just needed to be passed through the cli app. --- lib/import_export/import_export.rb | 4 ++-- script/discourse | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/import_export/import_export.rb b/lib/import_export/import_export.rb index 715a4bea5e..4bb6fa9250 100644 --- a/lib/import_export/import_export.rb +++ b/lib/import_export/import_export.rb @@ -6,8 +6,8 @@ require "json" module ImportExport - def self.export_category(category_id) - ImportExport::CategoryExporter.new(category_id).perform.save_to_file + def self.export_category(category_id, filename=nil) + ImportExport::CategoryExporter.new(category_id).perform.save_to_file(filename) end def self.import_category(filename) diff --git a/script/discourse b/script/discourse index fb91f986f4..af03de89b3 100755 --- a/script/discourse +++ b/script/discourse @@ -135,12 +135,12 @@ class DiscourseCLI < Thor end desc "export_category", "Export a category, all its topics, and all users who posted in those topics" - def export_category(category_id) + def export_category(category_id, filename=nil) raise "Category id argument is missing!" unless category_id load_rails load_import_export - ImportExport.export_category(category_id) + ImportExport.export_category(category_id, filename) puts "", "Done", "" end From b1e68390f4c4ab2fa0e94e3ce37f5731b8cc1686 Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 13 Feb 2016 17:49:26 +1100 Subject: [PATCH 074/140] FIX: false overrides should be permitted via ENV --- lib/site_setting_extension.rb | 2 +- spec/components/site_setting_extension_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index a6a46d6390..7912d8dbea 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -121,7 +121,7 @@ module SiteSettingExtension # exists it will be used instead of the setting and the setting will be hidden. # Useful for things like API keys on multisite. if opts[:shadowed_by_global] && GlobalSetting.respond_to?(name) - if (val = GlobalSetting.send(name)).present? + unless (val = GlobalSetting.send(name)) == ''.freeze hidden_settings << name shadowed_settings << name current_value = val diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index 8d9fd2eb82..f6982e8ef0 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -470,6 +470,18 @@ describe SiteSettingExtension do end end + context "with a false override" do + before do + GlobalSetting.stubs(:bool).returns(false) + settings.setting(:bool, true, shadowed_by_global: true) + settings.refresh! + end + + it "should return default cause nothing is set" do + expect(settings.bool).to eq(false) + end + end + context "with global setting" do before do GlobalSetting.stubs(:trout_api_key).returns('purringcat') From 2c0b36cb72e6403dc0b494f7a79bc81d8581503b Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sat, 13 Feb 2016 15:58:52 -0800 Subject: [PATCH 075/140] omit needless words --- config/locales/server.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index f0f4c26cd4..2e7ef47687 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1290,7 +1290,7 @@ en: moved_post: "%{display_username} moved your post to %{link}" private_message: "%{display_username} sent you a message: %{link}" invited_to_private_message: "%{display_username} invited you to a message: %{link}" - invited_to_topic: "%{display_username} invited you to a topic: %{link}" + invited_to_topic: "%{display_username} invited you to %{link}" invitee_accepted: "%{display_username} accepted your invitation" linked: "%{display_username} linked you in %{link}" granted_badge: "You earned %{link}" @@ -2044,7 +2044,7 @@ en: Please visit this link to view the message: %{base_url}%{url} user_invited_to_topic: - subject_template: "[%{site_name}] %{username} invited you to a topic '%{topic_title}'" + subject_template: "[%{site_name}] %{username} invited you to '%{topic_title}'" text_body_template: | %{username} invited you to a discussion From 8e9a8472f4714a7e2e63e669aaa7b58c9c8cd0e8 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 15 Feb 2016 10:56:39 +1100 Subject: [PATCH 076/140] FEATURE: don't move muted messages back into inbox --- app/models/user_archived_message.rb | 6 ++++++ spec/models/user_archived_message_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 spec/models/user_archived_message_spec.rb diff --git a/app/models/user_archived_message.rb b/app/models/user_archived_message.rb index 3a54b6ea6d..0617c76f0b 100644 --- a/app/models/user_archived_message.rb +++ b/app/models/user_archived_message.rb @@ -3,6 +3,12 @@ class UserArchivedMessage < ActiveRecord::Base belongs_to :topic def self.move_to_inbox!(user_id, topic_id) + return if (TopicUser.where( + user_id: user_id, + topic_id: topic_id, + notification_level: TopicUser.notification_levels[:muted] + ).exists?) + UserArchivedMessage.where(user_id: user_id, topic_id: topic_id).destroy_all MessageBus.publish("/topic/#{topic_id}", {type: "move_to_inbox"}, user_ids: [user_id]) end diff --git a/spec/models/user_archived_message_spec.rb b/spec/models/user_archived_message_spec.rb new file mode 100644 index 0000000000..cb3567b5db --- /dev/null +++ b/spec/models/user_archived_message_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe UserArchivedMessage do + it 'Does not move archived muted messages back to inbox' do + user = Fabricate(:admin) + user2 = Fabricate(:admin) + + topic = create_post(user: user, + skip_validations: true, + target_usernames: [user2.username,user.username].join(","), + archetype: Archetype.private_message).topic + + UserArchivedMessage.archive!(user.id, topic.id) + expect(topic.message_archived?(user)).to eq(true) + + TopicUser.change(user.id, topic.id, notification_level: TopicUser.notification_levels[:muted]) + UserArchivedMessage.move_to_inbox!(user.id, topic.id) + expect(topic.message_archived?(user)).to eq(true) + end +end + From 1f062ae2fde43a44f407f4af91a190c8c02fb1e0 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Feb 2016 18:54:53 +1100 Subject: [PATCH 077/140] PERF: improve performance of consistency query --- app/models/category_user.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/models/category_user.rb b/app/models/category_user.rb index e7138d91da..7f0ab17a88 100644 --- a/app/models/category_user.rb +++ b/app/models/category_user.rb @@ -95,7 +95,14 @@ class CategoryUser < ActiveRecord::Base end def self.ensure_consistency! - exec_sql("DELETE FROM category_users WHERE user_id NOT IN (SELECT id FROM users)") + exec_sql < Date: Mon, 15 Feb 2016 19:29:35 +1100 Subject: [PATCH 078/140] FIX: Always ensure notifications are treated as read once clicked UX: improve messaging so notifications list is far more stable PERF: improve performance of notifcation lookup queries - Add feature "SetTransientHeader" that allows shipping info to server in the next Ajax request - remove local storage hack used for notifications - amend lookupStale to return hydrated objects, move logic into store - stop magically clearing various notifications (likes, invitee accepted, group_summary, granted badge) --- .../discourse/adapters/notification.js.es6 | 3 +- .../discourse/adapters/rest.js.es6 | 24 +++++++++-- .../components/notification-item.js.es6 | 4 ++ .../discourse/components/user-menu.js.es6 | 2 +- .../subscribe-user-notifications.js.es6 | 30 +++++++++---- .../discourse/lib/stale-result.js.es6 | 12 ------ .../javascripts/discourse/mixins/ajax.js | 10 +++++ .../mixins/stale-local-storage.js.es6 | 34 --------------- .../javascripts/discourse/models/store.js.es6 | 38 +++++++++++++--- app/assets/javascripts/main_include.js | 1 - app/controllers/application_controller.rb | 23 ++++++++++ app/models/notification.rb | 30 +++++++++---- app/models/user.rb | 43 ++++++++++++++----- ...28_add_unread_pm_index_to_notifications.rb | 6 +++ .../application_controller_spec.rb | 29 +++++++++++++ 15 files changed, 203 insertions(+), 86 deletions(-) delete mode 100644 app/assets/javascripts/discourse/lib/stale-result.js.es6 delete mode 100644 app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 create mode 100644 db/migrate/20160215075528_add_unread_pm_index_to_notifications.rb diff --git a/app/assets/javascripts/discourse/adapters/notification.js.es6 b/app/assets/javascripts/discourse/adapters/notification.js.es6 index f8298a4307..ddee883f62 100644 --- a/app/assets/javascripts/discourse/adapters/notification.js.es6 +++ b/app/assets/javascripts/discourse/adapters/notification.js.es6 @@ -1,4 +1,3 @@ import RestAdapter from 'discourse/adapters/rest'; -import StaleLocalStorage from 'discourse/mixins/stale-local-storage'; -export default RestAdapter.extend(StaleLocalStorage); +export default RestAdapter.extend({cache: true}); diff --git a/app/assets/javascripts/discourse/adapters/rest.js.es6 b/app/assets/javascripts/discourse/adapters/rest.js.es6 index 1404e76d50..5faaf6d0bf 100644 --- a/app/assets/javascripts/discourse/adapters/rest.js.es6 +++ b/app/assets/javascripts/discourse/adapters/rest.js.es6 @@ -1,6 +1,8 @@ -import StaleResult from 'discourse/lib/stale-result'; +import { hashString } from 'discourse/lib/hash'; + const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host']; + export function Result(payload, responseJson) { this.payload = payload; this.responseJson = responseJson; @@ -19,6 +21,15 @@ function rethrow(error) { export default Ember.Object.extend({ + + storageKey(type, findArgs, options) { + if (options && options.cacheKey) { + return options.cacheKey; + } + const hashedArgs = Math.abs(hashString(JSON.stringify(findArgs))); + return `${type}_${hashedArgs}`; + }, + basePath(store, type) { if (ADMIN_MODELS.indexOf(type.replace('_', '-')) !== -1) { return "/admin/"; } return "/"; @@ -56,8 +67,15 @@ export default Ember.Object.extend({ return ajax(this.pathFor(store, type, findArgs)).catch(rethrow); }, - findStale() { - return new StaleResult(); + findStale(store, type, findArgs, options) { + if (this.cached) { + return this.cached[this.storageKey(type, findArgs, options)]; + } + }, + + cacheFind(store, type, findArgs, opts, hydrated) { + this.cached = this.cached || {}; + this.cached[this.storageKey(type,findArgs,opts)] = hydrated; }, update(store, type, id, attrs) { diff --git a/app/assets/javascripts/discourse/components/notification-item.js.es6 b/app/assets/javascripts/discourse/components/notification-item.js.es6 index 6818ba5a3d..f900c1b274 100644 --- a/app/assets/javascripts/discourse/components/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/components/notification-item.js.es6 @@ -61,6 +61,10 @@ export default Ember.Component.extend({ _markRead: function(){ this.$('a').click(() => { this.set('notification.read', true); + Discourse.setTransientHeader("Discourse-Clear-Notifications", this.get('notification.id')); + if (document && document.cookie) { + document.cookie = `cn=${this.get('notification.id')}; expires=Fri, 31 Dec 9999 23:59:59 GMT`; + } return true; }); }.on('didInsertElement'), diff --git a/app/assets/javascripts/discourse/components/user-menu.js.es6 b/app/assets/javascripts/discourse/components/user-menu.js.es6 index 506fc98d78..d864f23ba4 100644 --- a/app/assets/javascripts/discourse/components/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/user-menu.js.es6 @@ -54,7 +54,7 @@ export default Ember.Component.extend({ // TODO: It's a bit odd to use the store in a component, but this one really // wants to reach out and grab notifications const store = this.container.lookup('store:main'); - const stale = store.findStale('notification', {recent: true, limit }, {storageKey: 'recent-notifications'}); + const stale = store.findStale('notification', {recent: true, limit }, {cacheKey: 'recent-notifications'}); if (stale.hasResults) { const results = stale.results; diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 index 79157a123a..5923dfca87 100644 --- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 @@ -9,10 +9,11 @@ export default { site = container.lookup('site:main'), siteSettings = container.lookup('site-settings:main'), bus = container.lookup('message-bus:main'), - keyValueStore = container.lookup('key-value-store:main'); + keyValueStore = container.lookup('key-value-store:main'), + store = container.lookup('store:main'); - // clear old cached notifications - // they could be a week old for all we know + // clear old cached notifications, we used to store in local storage + // TODO 2017 delete this line keyValueStore.remove('recent-notifications'); if (user) { @@ -40,12 +41,12 @@ export default { user.set('lastNotificationChange', new Date()); } - var stale = keyValueStore.getObject('recent-notifications'); + const stale = store.findStale('notification', {}, {cacheKey: 'recent-notifications'}); const lastNotification = data.last_notification && data.last_notification.notification; - if (stale && stale.notifications && lastNotification) { + if (stale && stale.hasResults && lastNotification) { - const oldNotifications = stale.notifications; + const oldNotifications = stale.results.get('content'); const staleIndex = _.findIndex(oldNotifications, {id: lastNotification.id}); if (staleIndex > -1) { @@ -61,8 +62,21 @@ export default { insertPosition = insertPosition === -1 ? oldNotifications.length - 1 : insertPosition; } - oldNotifications.splice(insertPosition, 0, lastNotification); - keyValueStore.setItem('recent-notifications', JSON.stringify(stale)); + oldNotifications.splice(insertPosition, 0, Em.Object.create(lastNotification)); + + var idx=0; + data.recent.forEach((info)=> { + var old = oldNotifications[idx]; + if (old) { + if (old.get('id') !== info[0]) { + oldNotifications.splice(idx, 1); + return; + } else if (old.get('read') !== info[1]) { + old.set('read', info[1]); + } + } + idx += 1; + }); } }, user.notification_channel_position); diff --git a/app/assets/javascripts/discourse/lib/stale-result.js.es6 b/app/assets/javascripts/discourse/lib/stale-result.js.es6 deleted file mode 100644 index 1ae29e5832..0000000000 --- a/app/assets/javascripts/discourse/lib/stale-result.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -const StaleResult = function() { - this.hasResults = false; -}; - -StaleResult.prototype.setResults = function(results) { - if (results) { - this.results = results; - this.hasResults = true; - } -}; - -export default StaleResult; diff --git a/app/assets/javascripts/discourse/mixins/ajax.js b/app/assets/javascripts/discourse/mixins/ajax.js index fdd58cf2fe..36e42f5122 100644 --- a/app/assets/javascripts/discourse/mixins/ajax.js +++ b/app/assets/javascripts/discourse/mixins/ajax.js @@ -3,9 +3,14 @@ respect Discourse paths and the run loop. **/ var _trackView = false; +var _transientHeader = null; Discourse.Ajax = Em.Mixin.create({ + setTransientHeader: function(k, v) { + _transientHeader = {key: k, value: v}; + }, + viewTrackingRequired: function() { _trackView = true; }, @@ -43,6 +48,11 @@ Discourse.Ajax = Em.Mixin.create({ args.headers = args.headers || {}; + if (_transientHeader) { + args.headers[_transientHeader.key] = _transientHeader.value; + _transientHeader = null; + } + if (_trackView && (!args.type || args.type === "GET")) { _trackView = false; // DON'T CHANGE: rack is prepending "HTTP_" in the header's name diff --git a/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 b/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 deleted file mode 100644 index 19d8495162..0000000000 --- a/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 +++ /dev/null @@ -1,34 +0,0 @@ -import StaleResult from 'discourse/lib/stale-result'; -import { hashString } from 'discourse/lib/hash'; - -// Mix this in to an adapter to provide stale caching in our key value store -export default { - storageKey(type, findArgs) { - const hashedArgs = Math.abs(hashString(JSON.stringify(findArgs))); - return `${type}_${hashedArgs}`; - }, - - findStale(store, type, findArgs, opts) { - const staleResult = new StaleResult(); - const key = (opts && opts.storageKey) || this.storageKey(type, findArgs); - try { - const stored = this.keyValueStore.getItem(key); - if (stored) { - const parsed = JSON.parse(stored); - staleResult.setResults(parsed); - } - } catch(e) { - // JSON parsing error - } - return staleResult; - }, - - find(store, type, findArgs, opts) { - const key = (opts && opts.storageKey) || this.storageKey(type, findArgs); - - return this._super(store, type, findArgs).then((results) => { - this.keyValueStore.setItem(key, JSON.stringify(results)); - return results; - }); - } -}; diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index a7f4edcf9b..9c66da96dd 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -73,19 +73,43 @@ export default Ember.Object.extend({ // refresh it in the background. findStale(type, findArgs, opts) { const stale = this.adapterFor(type).findStale(this, type, findArgs, opts); - if (stale.hasResults) { - stale.results = this._hydrateFindResults(stale.results, type, findArgs); - } - stale.refresh = () => this.find(type, findArgs, opts); - return stale; + return { + hasResults: (stale !== undefined), + results: stale, + refresh: () => this.find(type, findArgs, opts) + }; }, find(type, findArgs, opts) { - return this.adapterFor(type).find(this, type, findArgs, opts).then(result => { - return this._hydrateFindResults(result, type, findArgs, opts); + var adapter = this.adapterFor(type); + return adapter.find(this, type, findArgs, opts).then(result => { + var hydrated = this._hydrateFindResults(result, type, findArgs, opts); + if (adapter.cache) { + const stale = adapter.findStale(this, type, findArgs, opts); + hydrated = this._updateStale(stale, hydrated); + adapter.cacheFind(this, type, findArgs, opts, hydrated); + } + return hydrated; }); }, + _updateStale(stale, hydrated) { + if (!stale) { + return hydrated; + } + + hydrated.set('content', hydrated.get('content').map((item) => { + var staleItem = stale.content.findBy('id', item.get('id')); + if (staleItem) { + staleItem.setProperties(item); + } else { + staleItem = item; + } + return staleItem; + })); + return hydrated; + }, + refreshResults(resultSet, type, url) { const self = this; return Discourse.ajax(url).then(result => { diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 43379cc09c..15c4cb0747 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -7,7 +7,6 @@ //= require ./ember-addons/macro-alias //= require ./ember-addons/ember-computed-decorators //= require ./discourse/lib/hash -//= require ./discourse/lib/stale-result //= require ./discourse/lib/load-script //= require ./discourse/lib/notification-levels //= require ./discourse/lib/app-events diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 05f850e30d..6dd3f81ade 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -33,6 +33,7 @@ class ApplicationController < ActionController::Base end before_filter :set_current_user_for_logs + before_filter :clear_notifications before_filter :set_locale before_filter :set_mobile_view before_filter :inject_preview_style @@ -137,6 +138,28 @@ class ApplicationController < ActionController::Base response.headers["X-Discourse-Route"] = "#{controller_name}/#{action_name}" end + def clear_notifications + if current_user && !Discourse.readonly_mode? + + cookie_notifications = cookies['cn'.freeze] + notifications = request.headers['Discourse-Clear-Notifications'.freeze] + + if cookie_notifications + if notifications.present? + notifications += "," << cookie_notifications + else + notifications = cookie_notifications + end + end + + if notifications.present? + notification_ids = notifications.split(",").map(&:to_i) + Notification.where(user_id: current_user.id, id: notification_ids).update_all(read: true) + cookies.delete('cn') + end + end + end + def set_locale I18n.locale = current_user.try(:effective_locale) || SiteSetting.default_locale I18n.ensure_all_loaded! diff --git a/app/models/notification.rb b/app/models/notification.rb index bffb70dce1..d21875c8b2 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -130,15 +130,29 @@ class Notification < ActiveRecord::Base .to_a if notifications.present? - notifications += user - .notifications - .order('notifications.created_at DESC') - .where(read: false, notification_type: Notification.types[:private_message]) - .joins(:topic) - .where('notifications.id < ?', notifications.last.id) - .limit(count) - notifications.sort do |x,y| + ids = Notification.exec_sql(" + SELECT n.id FROM notifications n + WHERE + n.notification_type = 6 AND + n.user_id = #{user.id.to_i} AND + NOT read + ORDER BY n.id ASC + LIMIT #{count.to_i} + ").values.map do |x,_| + x.to_i + end + + if ids.length > 0 + notifications += user + .notifications + .order('notifications.created_at DESC') + .where(id: ids) + .joins(:topic) + .limit(count) + end + + notifications.uniq(&:id).sort do |x,y| if x.unread_pm? && !y.unread_pm? -1 elsif y.unread_pm? && !x.unread_pm? diff --git a/app/models/user.rb b/app/models/user.rb index 639846ee26..a1be4c16b9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -293,15 +293,6 @@ class User < ActiveRecord::Base def saw_notification_id(notification_id) User.where("id = ? and seen_notification_id < ?", id, notification_id) .update_all ["seen_notification_id = ?", notification_id] - - # some notifications are considered read once seen - Notification.where('user_id = ? AND NOT read AND notification_type IN (?)', id, [ - Notification.types[:granted_badge], - Notification.types[:invitee_accepted], - Notification.types[:group_message_summary], - Notification.types[:liked] - ]) - .update_all ["read = ?", true] end def publish_notifications_state @@ -310,11 +301,43 @@ class User < ActiveRecord::Base notification = notifications.visible.order('notifications.id desc').first json = NotificationSerializer.new(notification).as_json if notification + + sql = " + SELECT * FROM ( + SELECT n.id, n.read FROM notifications n + LEFT JOIN topics t ON n.topic_id = t.id + WHERE + t.deleted_at IS NULL AND + n.notification_type = :type AND + n.user_id = :user_id AND + NOT read + ORDER BY n.id DESC + LIMIT 20 + ) AS x + UNION ALL + SELECT * FROM ( + SELECT n.id, n.read FROM notifications n + LEFT JOIN topics t ON n.topic_id = t.id + WHERE + t.deleted_at IS NULL AND + (n.notification_type <> :type OR read) AND + n.user_id = :user_id + ORDER BY n.id DESC + LIMIT 20 + ) AS y + " + + recent = User.exec_sql(sql, user_id: id, + type: Notification.types[:private_message]).values.map do |id, read| + [id.to_i, read == 't'.freeze] + end + MessageBus.publish("/notification/#{id}", {unread_notifications: unread_notifications, unread_private_messages: unread_private_messages, total_unread_notifications: total_unread_notifications, - last_notification: json + last_notification: json, + recent: recent }, user_ids: [id] # only publish the notification to this user ) diff --git a/db/migrate/20160215075528_add_unread_pm_index_to_notifications.rb b/db/migrate/20160215075528_add_unread_pm_index_to_notifications.rb new file mode 100644 index 0000000000..382489e2fd --- /dev/null +++ b/db/migrate/20160215075528_add_unread_pm_index_to_notifications.rb @@ -0,0 +1,6 @@ +class AddUnreadPmIndexToNotifications < ActiveRecord::Migration + def change + # create index idxtmp on notifications(user_id, id) where notification_type = 6 AND NOT read + add_index :notifications, [:user_id, :id], unique: true, where: 'notification_type = 6 AND NOT read' + end +end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index fc1911efd1..48a217039c 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -85,6 +85,35 @@ describe TopicsController do end + describe 'clear_notifications' do + it 'correctly clears notifications if specified via cookie' do + notification = Fabricate(:notification) + log_in_user(notification.user) + + request.cookies['cn'] = "2828,100,#{notification.id}" + + get :show, {topic_id: 100} + + expect(response.cookies['cn']).to eq nil + + notification.reload + expect(notification.read).to eq true + + end + + it 'correctly clears notifications if specified via header' do + notification = Fabricate(:notification) + log_in_user(notification.user) + + request.headers['Discourse-Clear-Notifications'] = "2828,100,#{notification.id}" + + get :show, {topic_id: 100} + + notification.reload + expect(notification.read).to eq true + end + end + describe 'set_locale' do it 'sets the one the user prefers' do SiteSetting.stubs(:allow_user_locale).returns(true) From e083fb44f47a62b09c9222c8ac099f1378d4d86a Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Feb 2016 20:13:55 +1100 Subject: [PATCH 079/140] FIX: cope with unset notifications on the component --- .../javascripts/discourse/components/notification-item.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/discourse/components/notification-item.js.es6 b/app/assets/javascripts/discourse/components/notification-item.js.es6 index f900c1b274..844bf83edf 100644 --- a/app/assets/javascripts/discourse/components/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/components/notification-item.js.es6 @@ -71,6 +71,8 @@ export default Ember.Component.extend({ render(buffer) { const notification = this.get('notification'); + // since we are reusing views now sometimes this can be unset + if (!notification) { return; } const description = this.get('description'); const username = notification.get('data.display_username'); var text; From d7400dd10aacfc53bb9defcff9d264eb06ce8f5c Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Feb 2016 20:27:29 +1100 Subject: [PATCH 080/140] UX: Stop taking you to user page when compose private message is called --- .../discourse/routes/application.js.es6 | 15 ++++++++++++--- .../javascripts/discourse/routes/user.js.es6 | 15 --------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index 8c1637b7af..0ace34b5bb 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -53,9 +53,18 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { }, composePrivateMessage(user, post) { - const self = this; - this.transitionTo('userActivity', user).then(function () { - self.controllerFor('user-activity').send('composePrivateMessage', user, post); + + const recipient = user ? user.get('username') : '', + reply = post ? window.location.protocol + "//" + window.location.host + post.get("url") : null; + + // used only once, one less dependency + const Composer = require('discourse/models/composer').default; + return this.controllerFor('composer').open({ + action: Composer.PRIVATE_MESSAGE, + usernames: recipient, + archetypeId: 'private_message', + draftKey: 'new_private_message', + reply: reply }); }, diff --git a/app/assets/javascripts/discourse/routes/user.js.es6 b/app/assets/javascripts/discourse/routes/user.js.es6 index 71e803300d..271dfad479 100644 --- a/app/assets/javascripts/discourse/routes/user.js.es6 +++ b/app/assets/javascripts/discourse/routes/user.js.es6 @@ -11,21 +11,6 @@ export default Discourse.Route.extend({ }, actions: { - composePrivateMessage(user, post) { - const recipient = user ? user.get('username') : '', - reply = post ? window.location.protocol + "//" + window.location.host + post.get("url") : null; - - // used only once, one less dependency - const Composer = require('discourse/models/composer').default; - return this.controllerFor('composer').open({ - action: Composer.PRIVATE_MESSAGE, - usernames: recipient, - archetypeId: 'private_message', - draftKey: 'new_private_message', - reply: reply - }); - }, - willTransition(transition) { // will reset the indexStream when transitioning to routes that aren't "indexStream" // otherwise the "header" will jump From 482a65821b2aeab216f8996f24e6824201b5425e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 12 Feb 2016 13:31:26 -0500 Subject: [PATCH 081/140] FIX: Latest eslint doesn't recognize TypedArray --- test/javascripts/lib/utilities-test.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index 003c40bb15..3cea33cbed 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -1,3 +1,4 @@ +/* global Int8Array:true */ import { blank } from 'helpers/qunit-helpers'; module("Discourse.Utilities"); From 40b099f1a6eb88e6250075981e319882bffc3de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 15 Feb 2016 12:34:45 +0100 Subject: [PATCH 082/140] FIX: keep whitespaces when replacing direct link to external images with local images --- app/jobs/regular/pull_hotlinked_images.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 9da1f35393..379ad102ec 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -63,7 +63,7 @@ module Jobs # Markdown reference - [x]: http:// raw.gsub!(/\[([^\]]+)\]:\s?#{escaped_src}/) { "[#{$1}]: #{url}" } # Direct link - raw.gsub!(/^#{escaped_src}\s?$/, "") + raw.gsub!(/^#{escaped_src}(\s?)$/) { "#{$1}" } end rescue => e Rails.logger.info("Failed to pull hotlinked image: #{src} post:#{post_id}\n" + e.message + "\n" + e.backtrace.join("\n")) From 2af587005bc639f6b41bfafd7e4cbf5736222086 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 15 Feb 2016 23:05:16 +0800 Subject: [PATCH 083/140] FIX: find_by_attribute method in Rails 4.5 is case insensitive. * https://github.com/rails/rails/pull/23690 --- app/models/discourse_single_sign_on.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index 894d545ada..492e4a7041 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -81,7 +81,7 @@ class DiscourseSingleSignOn < SingleSignOn private def match_email_or_create_user(ip_address) - user = User.find_by_email(email) + user = User.find_by(email: email) try_name = name.blank? ? nil : name try_username = username.blank? ? nil : username From 4ad5660615a4026b332a137a3977cd4a0d6e3b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 15 Feb 2016 17:53:07 +0100 Subject: [PATCH 084/140] add slightly more logs when skipping email notifications --- app/jobs/regular/user_email.rb | 40 +++++++++++++++++----------------- lib/email/sender.rb | 6 ++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/jobs/regular/user_email.rb b/app/jobs/regular/user_email.rb index 14f34b42c2..404dd07f8e 100644 --- a/app/jobs/regular/user_email.rb +++ b/app/jobs/regular/user_email.rb @@ -6,19 +6,20 @@ module Jobs class UserEmail < Jobs::Base def execute(args) - notification, post = nil - raise Discourse::InvalidParameters.new(:user_id) unless args[:user_id].present? raise Discourse::InvalidParameters.new(:type) unless args[:type].present? + post = nil + notification = nil type = args[:type] - user = User.find_by(id: args[:user_id]) + to_address = args[:to_address].presence || user.try(:email).presence || "no_email_found" + + set_skip_context(type, args[:user_id], to_address) - set_skip_context(type, args[:user_id], args[:to_address].presence || user.try(:email).presence || "no_email_found") return skip(I18n.t("email_log.no_user", user_id: args[:user_id])) unless user - if args[:post_id] + if args[:post_id].present? post = Post.find_by(id: args[:post_id]) return skip(I18n.t('email_log.post_not_found', post_id: args[:post_id])) unless post.present? end @@ -27,18 +28,17 @@ module Jobs notification = Notification.find_by(id: args[:notification_id]) end - message, skip_reason = message_for_email( user, - post, - type, - notification, - args[:notification_type], - args[:notification_data_hash], - args[:email_token], - args[:to_address] ) - + message, skip_reason = message_for_email(user, + post, + type, + notification, + args[:notification_type], + args[:notification_data_hash], + args[:email_token], + args[:to_address]) if message - Email::Sender.new(message, args[:type], user).send + Email::Sender.new(message, type, user).send else skip_reason end @@ -51,8 +51,8 @@ module Jobs NOTIFICATIONS_SENT_BY_MAILING_LIST ||= Set.new %w{posted replied mentioned group_mentioned quoted} def message_for_email(user, post, type, notification, - notification_type=nil, notification_data_hash=nil, - email_token=nil, to_address=nil) + notification_type=nil, notification_data_hash=nil, + email_token=nil, to_address=nil) set_skip_context(type, user.id, to_address || user.email) @@ -75,7 +75,7 @@ module Jobs end if notification || notification_type - email_args[:notification_type] ||= notification_type || notification.try(:notification_type) + email_args[:notification_type] ||= notification_type || notification.try(:notification_type) email_args[:notification_data_hash] ||= notification_data_hash || notification.try(:data_hash) unless String === email_args[:notification_type] @@ -113,7 +113,7 @@ module Jobs # Update the to address if we have a custom one if message && to_address.present? - message.to = [to_address] + message.to = to_address end [message, nil] @@ -143,7 +143,7 @@ module Jobs to_address: @skip_context[:to_address], user_id: @skip_context[:user_id], skipped: true, - skipped_reason: reason, + skipped_reason: "[UserEmail] #{reason}", ) end diff --git a/lib/email/sender.rb b/lib/email/sender.rb index e1f81c84fb..f0cef9ff22 100644 --- a/lib/email/sender.rb +++ b/lib/email/sender.rb @@ -23,7 +23,7 @@ module Email def send return if SiteSetting.disable_emails - return skip(I18n.t('email_log.message_blank')) if @message.blank? + return skip(I18n.t('email_log.message_blank')) if @message.blank? return skip(I18n.t('email_log.message_to_blank')) if @message.to.blank? if @message.text_part @@ -135,7 +135,7 @@ module Email def to_address @to_address ||= begin - to = @message ? @message.to : nil + to = @message.try(:to) to = to.first if Array === to to.presence || "no_email_found" end @@ -167,7 +167,7 @@ module Email to_address: to_address, user_id: @user.try(:id), skipped: true, - skipped_reason: reason + skipped_reason: "[Sender] #{reason}" ) end From c75360f8098673a5fdbdffd4a2f3cadf6b746639 Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Mon, 15 Feb 2016 15:59:31 -0800 Subject: [PATCH 085/140] Capitalize "ip" in "ip address" --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 97dbc78ac3..cfab4a6dc0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -299,7 +299,7 @@ en: other: "This topic has {{count}} posts awaiting approval" confirm: "Save Changes" - delete_prompt: "Are you sure you want to delete %{username}? This will remove all of their posts and block their email and ip address." + delete_prompt: "Are you sure you want to delete %{username}? This will remove all of their posts and block their email and IP address." approval: title: "Post Needs Approval" From 2b689d45ff4e01c757faa23d6279493c3bd68838 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 16 Feb 2016 11:52:33 +1100 Subject: [PATCH 086/140] Revert "save height on small screens" This reverts commit 37b5905b44e4334041e56eb941cfe8fcb33124c7. It is causing too much confusion for little gain --- app/assets/stylesheets/desktop/topic-post.scss | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index e5db38f831..8744a17edf 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -1038,16 +1038,3 @@ and (max-width : 767px) { } } - -@media all -and (max-height: 700px) { - - .post-menu-area { - margin-bottom: 0px; - margin-top: -18px; - } - - .topic-body { - padding: 5px 11px 2px; - } -} From 0c6e5befe462a233fb52570b5d6b7e86bcd2b697 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Tue, 16 Feb 2016 12:37:59 +0100 Subject: [PATCH 087/140] FIX: topic summary description text was conflicts with reply counter --- .../discourse/templates/components/toggle-summary.hbs | 4 ++-- config/locales/client.en.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/toggle-summary.hbs b/app/assets/javascripts/discourse/templates/components/toggle-summary.hbs index 6eed02a1cd..671b85d945 100644 --- a/app/assets/javascripts/discourse/templates/components/toggle-summary.hbs +++ b/app/assets/javascripts/discourse/templates/components/toggle-summary.hbs @@ -3,9 +3,9 @@ {{else}} {{#if topic.estimatedReadingTime}} -

    {{{i18n 'summary.description_time' count=topic.posts_count readingTime=topic.estimatedReadingTime}}}

    +

    {{{i18n 'summary.description_time' replyCount=topic.replyCount readingTime=topic.estimatedReadingTime}}}

    {{else}} -

    {{{i18n 'summary.description' count=topic.posts_count}}}

    +

    {{{i18n 'summary.description' replyCount=topic.replyCount}}}

    {{/if}} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index cfab4a6dc0..cdb5b7bc88 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -804,8 +804,8 @@ en: summary: enabled_description: "You're viewing a summary of this topic: the most interesting posts as determined by the community." - description: "There are {{count}} replies." - description_time: "There are {{count}} replies with an estimated read time of {{readingTime}} minutes." + description: "There are {{replyCount}} replies." + description_time: "There are {{replyCount}} replies with an estimated read time of {{readingTime}} minutes." enable: 'Summarize This Topic' disable: 'Show All Posts' From 81c6fb318b9648dbccf682579a049fb583660ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 16 Feb 2016 16:09:05 +0100 Subject: [PATCH 088/140] FIX: show name in preferences when SSO is enabled and is used to override names --- app/assets/javascripts/discourse/templates/user/preferences.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index 37966945a3..636227b757 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -28,7 +28,7 @@ {{#if model.can_edit_name}} {{text-field value=newNameInput classNames="input-xxlarge"}} {{else}} - {{name}} + {{model.name}} {{/if}}
    From bf96025507dd76ab6dc93762b6a8ca2510baac26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 16 Feb 2016 16:35:57 +0100 Subject: [PATCH 089/140] link email logs to the post that generate the email notification when available --- .../javascripts/admin/templates/email-sent.hbs | 8 +++++++- .../javascripts/admin/templates/email-skipped.hbs | 8 +++++++- app/controllers/admin/email_controller.rb | 2 +- app/jobs/regular/user_email.rb | 9 +++++---- app/serializers/email_log_serializer.rb | 12 +++++++++++- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/admin/templates/email-sent.hbs b/app/assets/javascripts/admin/templates/email-sent.hbs index 4e4f5dbf77..2f8785c6f5 100644 --- a/app/assets/javascripts/admin/templates/email-sent.hbs +++ b/app/assets/javascripts/admin/templates/email-sent.hbs @@ -30,7 +30,13 @@
    - + {{else}} diff --git a/app/assets/javascripts/admin/templates/email-skipped.hbs b/app/assets/javascripts/admin/templates/email-skipped.hbs index d983b09378..6ef4872566 100644 --- a/app/assets/javascripts/admin/templates/email-skipped.hbs +++ b/app/assets/javascripts/admin/templates/email-skipped.hbs @@ -30,7 +30,13 @@ - + {{else}} diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index 4b685c48e2..8ca208afb8 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -67,7 +67,7 @@ class Admin::EmailController < Admin::AdminController private def filter_email_logs(email_logs, params) - email_logs = email_logs.includes(:user) + email_logs = email_logs.includes(:user, { post: :topic }) .references(:user) .order(created_at: :desc) .offset(params[:offset] || 0) diff --git a/app/jobs/regular/user_email.rb b/app/jobs/regular/user_email.rb index 404dd07f8e..6830ec2e64 100644 --- a/app/jobs/regular/user_email.rb +++ b/app/jobs/regular/user_email.rb @@ -15,7 +15,7 @@ module Jobs user = User.find_by(id: args[:user_id]) to_address = args[:to_address].presence || user.try(:email).presence || "no_email_found" - set_skip_context(type, args[:user_id], to_address) + set_skip_context(type, args[:user_id], to_address, args[:post_id]) return skip(I18n.t("email_log.no_user", user_id: args[:user_id])) unless user @@ -44,8 +44,8 @@ module Jobs end end - def set_skip_context(type, user_id, to_address) - @skip_context = { type: type, user_id: user_id, to_address: to_address } + def set_skip_context(type, user_id, to_address, post_id) + @skip_context = { type: type, user_id: user_id, to_address: to_address, post_id: post_id } end NOTIFICATIONS_SENT_BY_MAILING_LIST ||= Set.new %w{posted replied mentioned group_mentioned quoted} @@ -54,7 +54,7 @@ module Jobs notification_type=nil, notification_data_hash=nil, email_token=nil, to_address=nil) - set_skip_context(type, user.id, to_address || user.email) + set_skip_context(type, user.id, to_address || user.email, post.try(:id)) return skip_message(I18n.t("email_log.anonymous_user")) if user.anonymous? return skip_message(I18n.t("email_log.suspended_not_pm")) if user.suspended? && type != :user_private_message @@ -142,6 +142,7 @@ module Jobs email_type: @skip_context[:type], to_address: @skip_context[:to_address], user_id: @skip_context[:user_id], + post_id: @skip_context[:post_id], skipped: true, skipped_reason: "[UserEmail] #{reason}", ) diff --git a/app/serializers/email_log_serializer.rb b/app/serializers/email_log_serializer.rb index 977e3ee93b..07f336d4e3 100644 --- a/app/serializers/email_log_serializer.rb +++ b/app/serializers/email_log_serializer.rb @@ -7,11 +7,21 @@ class EmailLogSerializer < ApplicationSerializer :user_id, :created_at, :skipped, - :skipped_reason + :skipped_reason, + :post_url has_one :user, serializer: BasicUserSerializer, embed: :objects def include_skipped_reason? object.skipped end + + def post_url + object.post.url + end + + def include_post_url? + object.post.present? + end + end From 3811b8aa4c9fc08bf84d5da55b7927f4e300b9b5 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 16 Feb 2016 12:25:01 -0500 Subject: [PATCH 090/140] `withPluginAPI` shim to updated plugins will not raise errors --- app/assets/javascripts/discourse/lib/plugin-api.js.es6 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index b7a7ece912..f5d66d738a 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -15,3 +15,8 @@ export function decorateCooked(container, cb) { decorate(container.lookupFactory('view:embedded-post'), 'didInsertElement', cb); decorate(container.lookupFactory('view:user-stream'), 'didInsertElement', cb); } + +// Will be backported so plugins in the new format will not raise errors +export function withPluginApi(version) { + console.warn(`Plugin API v${version} is not supported`); +} From 63b9d1c64588bb2d7a3fe3c84741edf0cd836a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 16 Feb 2016 18:29:23 +0100 Subject: [PATCH 091/140] FIX: sends an email notifcation when a user's post is linked --- app/controllers/admin/email_templates_controller.rb | 3 ++- app/mailers/user_notifications.rb | 7 +++++++ app/models/user_email_observer.rb | 4 ++++ config/locales/server.en.yml | 12 ++++++++++++ spec/models/user_email_observer_spec.rb | 8 ++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index 9d872c9984..ccbb3eaf69 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -26,7 +26,8 @@ class Admin::EmailTemplatesController < Admin::AdminController "user_notifications.user_invited_to_private_message_pm", "user_notifications.user_invited_to_topic", "user_notifications.user_mentioned", "user_notifications.user_posted", "user_notifications.user_posted_pm", - "user_notifications.user_quoted", "user_notifications.user_replied"] + "user_notifications.user_quoted", "user_notifications.user_replied", + "user_notifications.user_linked"] end def show diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 5172942781..2f4423cb14 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -110,6 +110,13 @@ class UserNotifications < ActionMailer::Base notification_email(user, opts) end + def user_linked(user, opts) + opts[:allow_reply_by_email] = true + opts[:use_site_subject] = true + opts[:show_category_in_subject] = true + notification_email(user, opts) + end + def user_mentioned(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true diff --git a/app/models/user_email_observer.rb b/app/models/user_email_observer.rb index db9dbdc73e..31128235ef 100644 --- a/app/models/user_email_observer.rb +++ b/app/models/user_email_observer.rb @@ -28,6 +28,10 @@ class UserEmailObserver < ActiveRecord::Observer enqueue :user_replied end + def linked + enqueue :user_linked + end + def private_message enqueue_private(:user_private_message) end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2e7ef47687..281d536546 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2095,6 +2095,18 @@ en: --- %{respond_instructions} + user_linked: + subject_template: "[%{site_name}] %{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + --- + %{respond_instructions} + user_mentioned: subject_template: "[%{site_name}] %{topic_title}" text_body_template: | diff --git a/spec/models/user_email_observer_spec.rb b/spec/models/user_email_observer_spec.rb index 0f1150c4dd..c0b8da164e 100644 --- a/spec/models/user_email_observer_spec.rb +++ b/spec/models/user_email_observer_spec.rb @@ -99,6 +99,14 @@ describe UserEmailObserver do include_examples "enqueue_public" end + context 'user_linked' do + let(:type) { :user_linked } + let(:delay) { SiteSetting.email_time_window_mins.minutes } + let!(:notification) { create_notification(11) } + + include_examples "enqueue_public" + end + context 'user_posted' do let(:type) { :user_posted } let(:delay) { SiteSetting.email_time_window_mins.minutes } From 63cda2262307d43160de0a84d538a646cd288723 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 16 Feb 2016 16:43:59 -0500 Subject: [PATCH 092/140] Upgrade `withPluginApi` to support non-api callbacks --- .../discourse/lib/plugin-api.js.es6 | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index f5d66d738a..5a700db5f2 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -16,7 +16,29 @@ export function decorateCooked(container, cb) { decorate(container.lookupFactory('view:user-stream'), 'didInsertElement', cb); } -// Will be backported so plugins in the new format will not raise errors -export function withPluginApi(version) { +// This is backported so plugins in the new format will not raise errors +// +// To upgrade your plugin for backwards compatibility, you can add code in this +// form: +// +// function newApiCode(api) { +// // api.xyz(); +// } +// +// function oldCode() { +// // your pre-PluginAPI code goes here. You will be able to delete this +// // code once the `PluginAPI` has been rolled out to all versions of +// // Discourse you want to support. +// } +// +// // `newApiCode` will use API version 0.1, if no API support then +// // `oldCode` will be called +// withPluginApi('0.1', newApiCode, { noApi: oldCode }); +// +export function withPluginApi(version, apiCodeCallback, opts) { console.warn(`Plugin API v${version} is not supported`); + + if (opts && opts.noApi) { + return opts.noApi(); + } } From 3829c78526d2a552ff61767f81694650151cac6b Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 17 Feb 2016 15:46:19 +1100 Subject: [PATCH 093/140] PERF: shift most user options out of the user table As it stands we load up user records quite frequently on the topic pages, this in turn pulls all the columns for the users being selected, just to discard them after they are loaded New structure keeps all options in a discrete table, this is better organised and allows us to easily add more column without worrying about bloating the user table --- .../javascripts/discourse/models/user.js.es6 | 32 ++++---- .../discourse/templates/user/preferences.hbs | 26 +++---- app/controllers/email_controller.rb | 9 ++- .../notify_mailing_list_subscribers.rb | 3 +- app/jobs/regular/user_email.rb | 6 +- app/jobs/scheduled/enqueue_digest_emails.rb | 4 +- app/mailers/user_notifications.rb | 2 +- app/models/user.rb | 62 ++------------- app/models/user_email_observer.rb | 4 +- app/models/user_option.rb | 29 +++++++ app/serializers/current_user_serializer.rb | 23 +++++- app/serializers/user_option_serializer.rb | 20 +++++ app/serializers/user_serializer.rb | 23 ++---- app/services/anonymous_shadow_creator.rb | 7 +- app/services/user_anonymizer.rb | 13 ++-- app/services/user_updater.rb | 24 ++++-- db/fixtures/009_users.rb | 8 +- db/migrate/20160225050317_add_user_options.rb | 76 +++++++++++++++++++ lib/guardian/post_guardian.rb | 2 +- spec/components/guardian_spec.rb | 4 +- spec/controllers/email_controller_spec.rb | 29 ++++--- spec/jobs/user_email_spec.rb | 7 +- spec/models/user_email_observer_spec.rb | 4 +- spec/models/user_spec.rb | 76 +++++++++++-------- spec/serializers/user_serializer_spec.rb | 21 ++++- .../services/anonymous_shadow_creator_spec.rb | 3 + spec/services/user_anonymizer_spec.rb | 14 ++-- spec/services/user_updater_spec.rb | 23 ++++-- 28 files changed, 363 insertions(+), 191 deletions(-) create mode 100644 app/models/user_option.rb create mode 100644 app/serializers/user_option_serializer.rb create mode 100644 db/migrate/20160225050317_add_user_options.rb diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index fd702adff7..a26f5e10e7 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -147,25 +147,29 @@ const User = RestModel.extend({ 'location', 'name', 'locale', - 'email_digests', - 'email_direct', - 'email_always', - 'email_private_messages', - 'dynamic_favicon', - 'digest_after_days', 'new_topic_duration_minutes', - 'external_links_in_new_tab', - 'mailing_list_mode', - 'enable_quoting', - 'disable_jump_reply', 'custom_fields', 'user_fields', 'muted_usernames', 'profile_background', - 'card_background', - 'automatically_unpin_topics' + 'card_background' ); + [ 'email_always', + 'mailing_list_mode', + 'external_links_in_new_tab', + 'email_digests', + 'email_direct', + 'email_private_messages', + 'dynamic_favicon', + 'enable_quoting', + 'disable_jump_reply', + 'automatically_unpin_topics', + 'digest_after_days' + ].forEach(s => { + data[s] = this.get(`user_option.${s}`); + }); + ['muted','watched','tracked'].forEach(s => { let cats = this.get(s + 'Categories').map(c => c.get('id')); // HACK: denote lack of categories @@ -174,7 +178,7 @@ const User = RestModel.extend({ }); if (!Discourse.SiteSettings.edit_history_visible_to_public) { - data['edit_history_public'] = this.get('edit_history_public'); + data['edit_history_public'] = this.get('user_option.edit_history_public'); } // TODO: We can remove this when migrated fully to rest model. @@ -184,7 +188,7 @@ const User = RestModel.extend({ type: 'PUT' }).then(result => { this.set('bio_excerpt', result.user.bio_excerpt); - const userProps = this.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); + const userProps = Em.getProperties(this.get('user-option'),'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); Discourse.User.current().setProperties(userProps); }).finally(() => { this.set('isSaving', false); diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index 636227b757..4c65524e82 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -169,17 +169,17 @@
    {{#if canReceiveDigest}} - {{preference-checkbox labelKey="user.email_digests.title" checked=model.email_digests}} - {{#if model.email_digests}} + {{preference-checkbox labelKey="user.email_digests.title" checked=model.user_option.email_digests}} + {{#if model.user_option.email_digests}}
    - {{combo-box valueAttribute="value" content=digestFrequencies value=model.digest_after_days}} + {{combo-box valueAttribute="value" content=digestFrequencies value=model.user_option.digest_after_days}}
    {{/if}} {{/if}} - {{preference-checkbox labelKey="user.email_private_messages" checked=model.email_private_messages}} - {{preference-checkbox labelKey="user.email_direct" checked=model.email_direct}} - {{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}} - {{preference-checkbox labelKey="user.email_always" checked=model.email_always}} + {{preference-checkbox labelKey="user.email_private_messages" checked=model.user_option.email_private_messages}} + {{preference-checkbox labelKey="user.email_direct" checked=model.user_option.email_direct}} + {{preference-checkbox labelKey="user.mailing_list_mode" checked=model.user_option.mailing_list_mode}} + {{preference-checkbox labelKey="user.email_always" checked=model.user_option.email_always}}
    {{#if siteSettings.email_time_window_mins}} @@ -209,12 +209,12 @@ {{combo-box valueAttribute="value" content=autoTrackDurations value=model.auto_track_topics_after_msecs}}
    - {{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.external_links_in_new_tab}} - {{preference-checkbox labelKey="user.enable_quoting" checked=model.enable_quoting}} - {{preference-checkbox labelKey="user.dynamic_favicon" checked=model.dynamic_favicon}} - {{preference-checkbox labelKey="user.disable_jump_reply" checked=model.disable_jump_reply}} + {{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.user_option.external_links_in_new_tab}} + {{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting}} + {{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}} + {{preference-checkbox labelKey="user.disable_jump_reply" checked=model.user_option.disable_jump_reply}} {{#unless siteSettings.edit_history_visible_to_public}} - {{preference-checkbox labelKey="user.edit_history_public" checked=model.edit_history_public}} + {{preference-checkbox labelKey="user.edit_history_public" checked=model.user_option.edit_history_public}} {{/unless}} {{plugin-outlet "user-custom-preferences"}} @@ -254,7 +254,7 @@ {{#if siteSettings.automatically_unpin_topics}}
    - {{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.automatically_unpin_topics}} + {{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}}
    {{/if}} diff --git a/app/controllers/email_controller.rb b/app/controllers/email_controller.rb index 4e0ff301af..22bbec0510 100644 --- a/app/controllers/email_controller.rb +++ b/app/controllers/email_controller.rb @@ -25,9 +25,12 @@ class EmailController < ApplicationController end if params[:from_all] - @user.update_columns(email_digests: false, email_direct: false, email_private_messages: false, email_always: false) + @user.user_option.update_columns(email_always: false, + email_digests: false, + email_direct: false, + email_private_messages: false) else - @user.update_column(:email_digests, false) + @user.user_option.update_column(:email_digests, false) end @success = true @@ -36,7 +39,7 @@ class EmailController < ApplicationController def resubscribe @user = DigestUnsubscribeKey.user_for_key(params[:key]) raise Discourse::NotFound unless @user.present? - @user.update_column(:email_digests, true) + @user.user_option.update_column(:email_digests, true) end end diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index c78e78f3e6..29436e4232 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -11,7 +11,8 @@ module Jobs users = User.activated.not_blocked.not_suspended.real - .where(mailing_list_mode: true) + .joins(:user_option) + .where(user_options: {mailing_list_mode: true}) .where('NOT EXISTS( SELECT 1 FROM topic_users tu diff --git a/app/jobs/regular/user_email.rb b/app/jobs/regular/user_email.rb index 6830ec2e64..ff38409c1b 100644 --- a/app/jobs/regular/user_email.rb +++ b/app/jobs/regular/user_email.rb @@ -62,7 +62,7 @@ module Jobs return if user.staged && type == :digest seen_recently = (user.last_seen_at.present? && user.last_seen_at > SiteSetting.email_time_window_mins.minutes.ago) - seen_recently = false if user.email_always || user.staged + seen_recently = false if user.user_option.email_always || user.staged email_args = {} @@ -85,14 +85,14 @@ module Jobs email_args[:notification_type] = email_args[:notification_type].to_s end - if user.mailing_list_mode? && + if user.user_option.mailing_list_mode? && !post.topic.private_message? && NOTIFICATIONS_SENT_BY_MAILING_LIST.include?(email_args[:notification_type]) # no need to log a reason when the mail was already sent via the mailing list job return [nil, nil] end - unless user.email_always? + unless user.user_option.email_always? if (notification && notification.read?) || (post && post.seen?(user)) return skip_message(I18n.t('email_log.notification_already_read')) end diff --git a/app/jobs/scheduled/enqueue_digest_emails.rb b/app/jobs/scheduled/enqueue_digest_emails.rb index 40cf0976d9..ea74f98ed2 100644 --- a/app/jobs/scheduled/enqueue_digest_emails.rb +++ b/app/jobs/scheduled/enqueue_digest_emails.rb @@ -15,8 +15,10 @@ module Jobs def target_user_ids # Users who want to receive emails and haven't been emailed in the last day query = User.real - .where(email_digests: true, active: true, staged: false) + .where(active: true, staged: false) + .joins(:user_option) .not_suspended + .where(user_options: {email_digests: true}) .where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)") .where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)") .where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * #{SiteSetting.delete_digest_email_after_days})") diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 2f4423cb14..52db55f8ab 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -321,7 +321,7 @@ class UserNotifications < ActionMailer::Base context: context, username: username, add_unsubscribe_link: !user.staged, - add_unsubscribe_via_email_link: user.mailing_list_mode, + add_unsubscribe_via_email_link: user.user_option.mailing_list_mode, unsubscribe_url: post.topic.unsubscribe_url, allow_reply_by_email: allow_reply_by_email, use_site_subject: use_site_subject, diff --git a/app/models/user.rb b/app/models/user.rb index a1be4c16b9..5634cbeaf3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -38,6 +38,7 @@ class User < ActiveRecord::Base has_many :user_archived_messages, dependent: :destroy + has_one :user_option, dependent: :destroy has_one :user_avatar, dependent: :destroy has_one :facebook_user_info, dependent: :destroy has_one :twitter_user_info, dependent: :destroy @@ -80,6 +81,7 @@ class User < ActiveRecord::Base after_create :create_email_token after_create :create_user_stat + after_create :create_user_option after_create :create_user_profile after_create :ensure_in_trust_level_group after_create :automatic_group_membership @@ -123,7 +125,7 @@ class User < ActiveRecord::Base # TODO-PERF: There is no indexes on any of these # and NotifyMailingListSubscribers does a select-all-and-loop - # may want to create an index on (active, blocked, suspended_till, mailing_list_mode)? + # may want to create an index on (active, blocked, suspended_till)? scope :blocked, -> { where(blocked: true) } scope :not_blocked, -> { where(blocked: false) } scope :suspended, -> { where('suspended_till IS NOT NULL AND suspended_till > ?', Time.zone.now) } @@ -911,6 +913,10 @@ class User < ActiveRecord::Base stat.save! end + def create_user_option + UserOption.create(user_id: id) + end + def create_email_token email_tokens.create(email: email) end @@ -965,21 +971,8 @@ class User < ActiveRecord::Base end def set_default_user_preferences - set_default_email_digest_frequency - set_default_email_private_messages - set_default_email_direct - set_default_email_mailing_list_mode - set_default_email_always - set_default_other_new_topic_duration_minutes set_default_other_auto_track_topics_after_msecs - set_default_other_external_links_in_new_tab - set_default_other_enable_quoting - set_default_other_dynamic_favicon - set_default_other_disable_jump_reply - set_default_other_edit_history_public - - set_default_topics_automatic_unpin # needed, otherwise the callback chain is broken... true @@ -1031,26 +1024,6 @@ class User < ActiveRecord::Base end end - def set_default_email_digest_frequency - if has_attribute?(:email_digests) - if SiteSetting.default_email_digest_frequency.to_i <= 0 - self.email_digests = false - else - self.email_digests = true - self.digest_after_days ||= SiteSetting.default_email_digest_frequency.to_i if has_attribute?(:digest_after_days) - end - end - end - - def set_default_email_mailing_list_mode - self.mailing_list_mode = SiteSetting.default_email_mailing_list_mode if has_attribute?(:mailing_list_mode) - end - - %w{private_messages direct always}.each do |s| - define_method("set_default_email_#{s}") do - self.send("email_#{s}=", SiteSetting.send("default_email_#{s}")) if has_attribute?("email_#{s}") - end - end %w{new_topic_duration_minutes auto_track_topics_after_msecs}.each do |s| define_method("set_default_other_#{s}") do @@ -1058,16 +1031,6 @@ class User < ActiveRecord::Base end end - %w{external_links_in_new_tab enable_quoting dynamic_favicon disable_jump_reply edit_history_public}.each do |s| - define_method("set_default_other_#{s}") do - self.send("#{s}=", SiteSetting.send("default_other_#{s}")) if has_attribute?(s) - end - end - - def set_default_topics_automatic_unpin - self.automatically_unpin_topics = SiteSetting.default_topics_automatic_unpin - end - end # == Schema Information @@ -1090,14 +1053,11 @@ end # last_seen_at :datetime # admin :boolean default(FALSE), not null # last_emailed_at :datetime -# email_digests :boolean not null # trust_level :integer not null # email_private_messages :boolean default(TRUE) -# email_direct :boolean default(TRUE), not null # approved :boolean default(FALSE), not null # approved_by_id :integer # approved_at :datetime -# digest_after_days :integer # previous_visit_at :datetime # suspended_at :datetime # suspended_till :datetime @@ -1107,24 +1067,16 @@ end # flag_level :integer default(0), not null # ip_address :inet # new_topic_duration_minutes :integer -# external_links_in_new_tab :boolean not null -# enable_quoting :boolean default(TRUE), not null # moderator :boolean default(FALSE) # blocked :boolean default(FALSE) -# dynamic_favicon :boolean default(FALSE), not null # title :string(255) # uploaded_avatar_id :integer -# email_always :boolean default(FALSE), not null -# mailing_list_mode :boolean default(FALSE), not null # primary_group_id :integer # locale :string(10) # registration_ip_address :inet # last_redirected_to_top_at :datetime -# disable_jump_reply :boolean default(FALSE), not null -# edit_history_public :boolean default(FALSE), not null # trust_level_locked :boolean default(FALSE), not null # staged :boolean default(FALSE), not null -# automatically_unpin_topics :boolean default(TRUE) # # Indexes # diff --git a/app/models/user_email_observer.rb b/app/models/user_email_observer.rb index 31128235ef..d153d01b50 100644 --- a/app/models/user_email_observer.rb +++ b/app/models/user_email_observer.rb @@ -64,12 +64,12 @@ class UserEmailObserver < ActiveRecord::Observer EMAILABLE_POST_TYPES ||= Set.new [Post.types[:regular], Post.types[:whisper]] def enqueue(type, delay=default_delay) - return unless notification.user.email_direct? + return unless notification.user.user_option.email_direct? perform_enqueue(type, delay) end def enqueue_private(type, delay=private_delay) - return unless notification.user.email_private_messages? + return unless notification.user.user_option.email_private_messages? perform_enqueue(type, delay) end diff --git a/app/models/user_option.rb b/app/models/user_option.rb new file mode 100644 index 0000000000..3dac2ded9e --- /dev/null +++ b/app/models/user_option.rb @@ -0,0 +1,29 @@ +class UserOption < ActiveRecord::Base + self.primary_key = :user_id + belongs_to :user + before_create :set_defaults + + def set_defaults + self.email_always = SiteSetting.default_email_always + self.mailing_list_mode = SiteSetting.default_email_mailing_list_mode + self.email_direct = SiteSetting.default_email_direct + self.automatically_unpin_topics = SiteSetting.default_topics_automatic_unpin + self.email_private_messages = SiteSetting.default_email_private_messages + + self.enable_quoting = SiteSetting.default_other_enable_quoting + self.external_links_in_new_tab = SiteSetting.default_other_external_links_in_new_tab + self.dynamic_favicon = SiteSetting.default_other_dynamic_favicon + self.disable_jump_reply = SiteSetting.default_other_disable_jump_reply + self.edit_history_public = SiteSetting.default_other_edit_history_public + + + if SiteSetting.default_email_digest_frequency.to_i <= 0 + self.email_digests = false + else + self.email_digests = true + self.digest_after_days ||= SiteSetting.default_email_digest_frequency.to_i + end + + true + end +end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index c576025cd5..5cf912d473 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -31,7 +31,8 @@ class CurrentUserSerializer < BasicUserSerializer :is_anonymous, :post_queue_new_count, :show_queued_posts, - :read_faq + :read_faq, + :automatically_unpin_topics def include_site_flagged_posts_count? object.staff? @@ -49,6 +50,26 @@ class CurrentUserSerializer < BasicUserSerializer object.user_stat.topic_reply_count end + def enable_quoting + object.user_option.enable_quoting + end + + def disable_jump_reply + object.user_option.disable_jump_reply + end + + def external_links_in_new_tab + object.user_option.external_links_in_new_tab + end + + def dynamic_favicon + object.user_option.dynamic_favicon + end + + def automatically_unpin_topics + object.user_option.automatically_unpin_topics + end + def site_flagged_posts_count PostAction.flagged_posts_count end diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb new file mode 100644 index 0000000000..77c9d68540 --- /dev/null +++ b/app/serializers/user_option_serializer.rb @@ -0,0 +1,20 @@ +class UserOptionSerializer < ApplicationSerializer + attributes :user_id, + :email_always, + :mailing_list_mode, + :email_digests, + :email_private_messages, + :email_direct, + :external_links_in_new_tab, + :dynamic_favicon, + :enable_quoting, + :disable_jump_reply, + :digest_after_days, + :automatically_unpin_topics, + :edit_history_public + + + def include_edit_history_public? + !SiteSetting.edit_history_visible_to_public + end +end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 480fbd9c61..1ad96ce240 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -61,40 +61,33 @@ class UserSerializer < BasicUserSerializer :uploaded_avatar_id, :badge_count, :has_title_badges, - :edit_history_public, :custom_fields, :user_fields, :topic_post_count, :pending_count, - :profile_view_count, - :automatically_unpin_topics + :profile_view_count has_one :invited_by, embed: :object, serializer: BasicUserSerializer has_many :groups, embed: :object, serializer: BasicGroupSerializer has_many :featured_user_badges, embed: :ids, serializer: UserBadgeSerializer, root: :user_badges has_one :card_badge, embed: :object, serializer: BadgeSerializer + has_one :user_option, embed: :object, serializer: UserOptionSerializer + + def include_user_option? + can_edit + end staff_attributes :post_count, :can_be_deleted, :can_delete_all_posts private_attributes :locale, - :email_digests, - :email_private_messages, - :email_direct, - :email_always, - :digest_after_days, - :mailing_list_mode, :auto_track_topics_after_msecs, :new_topic_duration_minutes, - :external_links_in_new_tab, - :dynamic_favicon, - :enable_quoting, :muted_category_ids, :tracked_category_ids, :watched_category_ids, :private_messages_stats, - :disable_jump_reply, :system_avatar_upload_id, :system_avatar_template, :gravatar_avatar_upload_id, @@ -322,10 +315,6 @@ class UserSerializer < BasicUserSerializer object.badges.where(allow_title: true).count > 0 end - def include_edit_history_public? - can_edit && !SiteSetting.edit_history_visible_to_public - end - def user_fields object.user_fields end diff --git a/app/services/anonymous_shadow_creator.rb b/app/services/anonymous_shadow_creator.rb index 18b7ecceaf..08c1d6a338 100644 --- a/app/services/anonymous_shadow_creator.rb +++ b/app/services/anonymous_shadow_creator.rb @@ -40,11 +40,14 @@ class AnonymousShadowCreator active: true, trust_level: 1, trust_level_locked: true, - email_private_messages: false, - email_digests: false, created_at: 1.day.ago # bypass new user restrictions ) + shadow.user_option.update_columns( + email_private_messages: false, + email_digests: false + ) + shadow.email_tokens.update_all(confirmed: true) shadow.activate diff --git a/app/services/user_anonymizer.rb b/app/services/user_anonymizer.rb index 48699b176a..e9ff162dbc 100644 --- a/app/services/user_anonymizer.rb +++ b/app/services/user_anonymizer.rb @@ -23,14 +23,17 @@ class UserAnonymizer @user.name = nil @user.date_of_birth = nil @user.title = nil - @user.email_digests = false - @user.email_private_messages = false - @user.email_direct = false - @user.email_always = false - @user.mailing_list_mode = false @user.uploaded_avatar_id = nil @user.save + options = @user.user_option + options.email_always = false + options.mailing_list_mode = false + options.email_digests = false + options.email_private_messages = false + options.email_direct = false + options.save + profile = @user.user_profile profile.destroy if profile @user.create_user_profile diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index c6f236d2b3..e028a0d14b 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -6,18 +6,19 @@ class UserUpdater muted_category_ids: :muted } - USER_ATTR = [ - :email_digests, + OPTION_ATTR = [ :email_always, + :mailing_list_mode, + :email_digests, :email_direct, :email_private_messages, :external_links_in_new_tab, :enable_quoting, :dynamic_favicon, - :mailing_list_mode, :disable_jump_reply, :edit_history_public, :automatically_unpin_topics, + :digest_after_days ] def initialize(actor, user) @@ -36,7 +37,6 @@ class UserUpdater user.name = attributes.fetch(:name) { user.name } user.locale = attributes.fetch(:locale) { user.locale } - user.digest_after_days = attributes.fetch(:digest_after_days) { user.digest_after_days } if attributes[:auto_track_topics_after_msecs] user.auto_track_topics_after_msecs = attributes[:auto_track_topics_after_msecs].to_i @@ -56,9 +56,19 @@ class UserUpdater end end - USER_ATTR.each do |attribute| + + save_options = false + OPTION_ATTR.each do |attribute| if attributes[attribute].present? - user.send("#{attribute}=", attributes[attribute] == 'true') + save_options = true + + + if [true,false].include?(user.user_option.send(attribute)) + val = attributes[attribute].to_s == 'true' + user.user_option.send("#{attribute}=", val) + else + user.user_option.send("#{attribute}=", attributes[attribute]) + end end end @@ -72,7 +82,7 @@ class UserUpdater update_muted_users(attributes[:muted_usernames]) end - user_profile.save && user.save + (!save_options || user.user_option.save) && user_profile.save && user.save end end diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index 7effb73775..e6c7486b96 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -16,12 +16,15 @@ User.seed do |u| u.active = true u.admin = true u.moderator = true - u.email_direct = false u.approved = true - u.email_private_messages = false u.trust_level = TrustLevel[4] end +UserOption.where(user_id: -1).update_all( + email_private_messages: false, + email_direct: false +) + Group.user_trust_level_change!(-1, TrustLevel[4]) # User for the smoke tests @@ -49,3 +52,4 @@ if ENV["SMOKE"] == "1" et.confirmed = true end end + diff --git a/db/migrate/20160225050317_add_user_options.rb b/db/migrate/20160225050317_add_user_options.rb new file mode 100644 index 0000000000..13df057cda --- /dev/null +++ b/db/migrate/20160225050317_add_user_options.rb @@ -0,0 +1,76 @@ +class AddUserOptions < ActiveRecord::Migration + def up + + create_table :user_options, id: false do |t| + t.integer :user_id, null: false + t.boolean :email_always, null: false, default: false + t.boolean :mailing_list_mode, null: false, default: false + t.boolean :email_digests + t.boolean :email_direct, null: false, default: true + t.boolean :email_private_messages, null: false, default: true + t.boolean :external_links_in_new_tab, null: false, default: false + t.boolean :enable_quoting, null: false, default: true + t.boolean :dynamic_favicon, null: false, default: false + t.boolean :disable_jump_reply, null: false, default: false + t.boolean :edit_history_public, null: false, default: false + t.boolean :automatically_unpin_topics, null: false, default: true + t.integer :digest_after_days + end + + add_index :user_options, [:user_id], unique: true + + execute < Date: Wed, 17 Feb 2016 17:47:47 +1100 Subject: [PATCH 094/140] FEATURE: start tracking information about migrations that run This commit adds a new tracking table that lets us know - When a migration ran - What version Discourse was at - How long it took - What version Rails was at The built in tracking in Rails is very limited, does not track this info --- ...0225050318_add_schema_migration_details.rb | 30 +++++++++++ .../schema_migration_details.rb | 54 +++++++++++++++++++ .../schema_migration_details_spec.rb | 32 +++++++++++ 3 files changed, 116 insertions(+) create mode 100644 db/migrate/20000225050318_add_schema_migration_details.rb create mode 100644 lib/freedom_patches/schema_migration_details.rb create mode 100644 spec/components/freedom_patches/schema_migration_details_spec.rb diff --git a/db/migrate/20000225050318_add_schema_migration_details.rb b/db/migrate/20000225050318_add_schema_migration_details.rb new file mode 100644 index 0000000000..dbf4959b17 --- /dev/null +++ b/db/migrate/20000225050318_add_schema_migration_details.rb @@ -0,0 +1,30 @@ +class AddSchemaMigrationDetails < ActiveRecord::Migration + def up + # schema_migrations table is way too thin, does not give info about + # duration of migration or the date it happened, this migration together with the + # monkey patch adds a lot of information to the migration table + + create_table :schema_migration_details do |t| + t.string :version, null: false + t.string :name + t.string :hostname + t.string :git_version + t.string :rails_version + t.integer :duration + t.string :direction # this really should be a pg enum type but annoying to wire up for little gain + t.datetime :created_at, null: false + end + + add_index :schema_migration_details, [:version] + + execute("INSERT INTO schema_migration_details(version, created_at) + SELECT version, current_timestamp + FROM schema_migrations + ORDER BY version + ") + end + + def down + drop_table :schema_migration_details + end +end diff --git a/lib/freedom_patches/schema_migration_details.rb b/lib/freedom_patches/schema_migration_details.rb new file mode 100644 index 0000000000..bbae956c11 --- /dev/null +++ b/lib/freedom_patches/schema_migration_details.rb @@ -0,0 +1,54 @@ +module FreedomPatches + module SchemaMigrationDetails + def exec_migration(conn, direction) + rval = nil + + time = Benchmark.measure do + rval=super + end + + sql = < 0 + expect(info.git_version).to eq Discourse.git_version + expect(info.direction).to eq "up" + expect(info.rails_version).to eq Rails.version + expect(info.filename).to eq migration.filename + expect(info.name).to eq "awesome_migration" + end +end From 6912aa9fd92dbe38a1e3c1c40d1e624f368792ab Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 17 Feb 2016 18:08:05 +1100 Subject: [PATCH 095/140] Remove superflous columns from the users table --- db/fixtures/009_users.rb | 34 +++++++++++++++++++ ...225050318_allow_defaults_on_users_table.rb | 10 ++++++ 2 files changed, 44 insertions(+) create mode 100644 db/migrate/20160225050318_allow_defaults_on_users_table.rb diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index e6c7486b96..4c5f118754 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -27,6 +27,40 @@ UserOption.where(user_id: -1).update_all( Group.user_trust_level_change!(-1, TrustLevel[4]) +# 60 minutes after our migration runs we need to exectue this code... +duration = Rails.env.production? ? 60 : 0 +if User.exec_sql("SELECT 1 FROM schema_migration_details + WHERE EXISTS( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'users' AND column_name = 'enable_quoting' + ) AND + name = 'AllowDefaultsOnUsersTable' AND + created_at < (current_timestamp at time zone 'UTC' - interval '#{duration} minutes') + ").to_a.length > 0 + + + User.transaction do + STDERR.puts "Removing superflous user columns!" + %w[ + email_always + mailing_list_mode + email_digests + email_direct + email_private_messages + external_links_in_new_tab + enable_quoting + dynamic_favicon + disable_jump_reply + edit_history_public + automatically_unpin_topics + digest_after_days + ].each do |column| + User.exec_sql("ALTER TABLE users DROP column #{column}") + end + + end +end + # User for the smoke tests if ENV["SMOKE"] == "1" smoke_user = User.seed do |u| diff --git a/db/migrate/20160225050318_allow_defaults_on_users_table.rb b/db/migrate/20160225050318_allow_defaults_on_users_table.rb new file mode 100644 index 0000000000..999e275fdd --- /dev/null +++ b/db/migrate/20160225050318_allow_defaults_on_users_table.rb @@ -0,0 +1,10 @@ +class AllowDefaultsOnUsersTable < ActiveRecord::Migration + def up + # we need to temporarily change table a bit to ensure we can insert new records + change_column :users, :email_digests, :boolean, null: false, default: true + change_column :users, :external_links_in_new_tab, :boolean, null: false, default: false + end + + def down + end +end From a5c5ac12fb9bf281ce947a854796c1e2d553ae05 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 17 Feb 2016 18:13:57 +1100 Subject: [PATCH 096/140] correct spec --- spec/components/freedom_patches/schema_migration_details_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/components/freedom_patches/schema_migration_details_spec.rb b/spec/components/freedom_patches/schema_migration_details_spec.rb index 86e960edb5..951f5f7c4f 100644 --- a/spec/components/freedom_patches/schema_migration_details_spec.rb +++ b/spec/components/freedom_patches/schema_migration_details_spec.rb @@ -26,7 +26,6 @@ describe FreedomPatches::SchemaMigrationDetails do expect(info.git_version).to eq Discourse.git_version expect(info.direction).to eq "up" expect(info.rails_version).to eq Rails.version - expect(info.filename).to eq migration.filename expect(info.name).to eq "awesome_migration" end end From 8981ca41f09e70039de43407658b38086f2ff774 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 17 Feb 2016 18:38:57 +1100 Subject: [PATCH 097/140] correct acceptance test --- app/assets/javascripts/discourse/models/user.js.es6 | 2 +- test/javascripts/fixtures/user_fixtures.js.es6 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index a26f5e10e7..516f7b1f7a 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -188,7 +188,7 @@ const User = RestModel.extend({ type: 'PUT' }).then(result => { this.set('bio_excerpt', result.user.bio_excerpt); - const userProps = Em.getProperties(this.get('user-option'),'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); + const userProps = Em.getProperties(this.get('user_option'),'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); Discourse.User.current().setProperties(userProps); }).finally(() => { this.set('isSaving', false); diff --git a/test/javascripts/fixtures/user_fixtures.js.es6 b/test/javascripts/fixtures/user_fixtures.js.es6 index 7a24b9b546..c18d6f2fe9 100644 --- a/test/javascripts/fixtures/user_fixtures.js.es6 +++ b/test/javascripts/fixtures/user_fixtures.js.es6 @@ -1,6 +1,6 @@ /*jshint maxlen:10000000 */ export default { -"/users/eviltrout.json": {"user_badges":[{"id":5870,"granted_at":"2014-05-16T02:39:38.388Z","badge_id":4,"user_id":19,"granted_by_id":-1},{"id":40673,"granted_at":"2014-03-31T14:23:18.060Z","post_id":7241,"post_number":19,"badge_id":23,"user_id":19,"granted_by_id":-1,"topic_id":3153},{"id":5868,"granted_at":"2014-05-16T02:39:38.380Z","badge_id":3,"user_id":19,"granted_by_id":-1}],"badges":[{"id":4,"name":"Leader","description":null,"grant_count":7,"allow_title":true,"multiple_grant":false,"icon":"fa-user","image":null,"listable":true,"enabled":true,"badge_grouping_id":4,"system":true,"badge_type_id":1},{"id":23,"name":"Great Share","description":null,"grant_count":14,"allow_title":false,"multiple_grant":true,"icon":"fa-certificate","image":null,"listable":true,"enabled":true,"badge_grouping_id":2,"system":true,"badge_type_id":1},{"id":3,"name":"Regular","description":null,"grant_count":30,"allow_title":true,"multiple_grant":false,"icon":"fa-user","image":null,"listable":true,"enabled":true,"badge_grouping_id":4,"system":true,"badge_type_id":2}],"badge_types":[{"id":1,"name":"Gold","sort_order":9},{"id":2,"name":"Silver","sort_order":8},{"id":3,"name":"Bronze","sort_order":7}],"users":[{"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"},{"id":-1,"username":"system","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/system/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"}],"topics":[{"id":3153,"title":"Is it better for Discourse to use JavaScript or CoffeeScript?","fancy_title":"Is it better for Discourse to use JavaScript or CoffeeScript?","slug":"is-it-better-for-discourse-to-use-javascript-or-coffeescript","posts_count":56}],"user":{"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png","name":"Robin Ward","email":"robin.ward@gmail.com","last_posted_at":"2015-05-07T15:23:35.074Z","last_seen_at":"2015-05-13T14:34:23.188Z","bio_raw":"Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.","bio_cooked":"

    Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.

    ","created_at":"2013-02-03T15:19:22.704Z","website":"http://eviltrout.com","location":"Toronto","can_edit":false,"can_edit_username":true,"can_edit_email":true,"can_edit_name":true,"stats":[{"action_type":13,"count":342,"id":null},{"action_type":12,"count":109,"id":null},{"action_type":4,"count":27,"id":null},{"action_type":5,"count":1607,"id":null},{"action_type":6,"count":771,"id":null},{"action_type":1,"count":333,"id":null},{"action_type":2,"count":2671,"id":null},{"action_type":7,"count":949,"id":null},{"action_type":9,"count":42,"id":null},{"action_type":3,"count":8,"id":null},{"action_type":11,"count":20,"id":null}],"can_send_private_messages":true,"can_send_private_message_to_user":false,"bio_excerpt":"Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.","trust_level":4,"moderator":true,"admin":true,"title":"co-founder","badge_count":23,"notification_count":3244,"has_title_badges":true,"custom_fields":{},"user_fields":{"1":"33"},"pending_count":0,"post_count":1987,"can_be_deleted":false,"can_delete_all_posts":false,"locale":"","email_digests":true,"email_private_messages":true,"email_direct":true,"email_always":true,"digest_after_days":7,"mailing_list_mode":false,"auto_track_topics_after_msecs":60000,"new_topic_duration_minutes":1440,"external_links_in_new_tab":false,"dynamic_favicon":true,"enable_quoting":true,"muted_category_ids":[],"tracked_category_ids":[],"watched_category_ids":[3],"private_messages_stats":{"all":101,"mine":13,"unread":3},"disable_jump_reply":false,"gravatar_avatar_upload_id":5275,"custom_avatar_upload_id":1573,"card_image_badge":"https://meta-discourse.global.ssl.fastly.net/uploads/default/36220/15b19c80dd99d5a5.png","card_image_badge_id":120,"muted_usernames":[],"invited_by":{"id":1,"username":"sam","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/sam/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"},"custom_groups":[{"id":44,"automatic":false,"name":"ubuntu","user_count":11,"alias_level":0,"visible":true,"automatic_membership_email_domains":null,"automatic_membership_retroactive":false,"primary_group":false,"title":null},{"id":47,"automatic":false,"name":"discourse","user_count":7,"alias_level":0,"visible":true,"automatic_membership_email_domains":null,"automatic_membership_retroactive":false,"primary_group":false,"title":null}],"featured_user_badge_ids":[5870,40673,5868],"card_badge":{"id":120,"name":"Garbage Man","description":"This Discourse developer successfully called something \"garbage!\"","grant_count":3,"allow_title":false,"multiple_grant":false,"icon":"https://meta-discourse.global.ssl.fastly.net/uploads/default/36220/15b19c80dd99d5a5.png","image":"https://meta-discourse.global.ssl.fastly.net/uploads/default/36220/15b19c80dd99d5a5.png","listable":false,"enabled":false,"badge_grouping_id":8,"system":false,"badge_type_id":3}}}, +"/users/eviltrout.json": {"user_badges":[{"id":5870,"granted_at":"2014-05-16T02:39:38.388Z","badge_id":4,"user_id":19,"granted_by_id":-1},{"id":40673,"granted_at":"2014-03-31T14:23:18.060Z","post_id":7241,"post_number":19,"badge_id":23,"user_id":19,"granted_by_id":-1,"topic_id":3153},{"id":5868,"granted_at":"2014-05-16T02:39:38.380Z","badge_id":3,"user_id":19,"granted_by_id":-1}],"badges":[{"id":4,"name":"Leader","description":null,"grant_count":7,"allow_title":true,"multiple_grant":false,"icon":"fa-user","image":null,"listable":true,"enabled":true,"badge_grouping_id":4,"system":true,"badge_type_id":1},{"id":23,"name":"Great Share","description":null,"grant_count":14,"allow_title":false,"multiple_grant":true,"icon":"fa-certificate","image":null,"listable":true,"enabled":true,"badge_grouping_id":2,"system":true,"badge_type_id":1},{"id":3,"name":"Regular","description":null,"grant_count":30,"allow_title":true,"multiple_grant":false,"icon":"fa-user","image":null,"listable":true,"enabled":true,"badge_grouping_id":4,"system":true,"badge_type_id":2}],"badge_types":[{"id":1,"name":"Gold","sort_order":9},{"id":2,"name":"Silver","sort_order":8},{"id":3,"name":"Bronze","sort_order":7}],"users":[{"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"},{"id":-1,"username":"system","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/system/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"}],"topics":[{"id":3153,"title":"Is it better for Discourse to use JavaScript or CoffeeScript?","fancy_title":"Is it better for Discourse to use JavaScript or CoffeeScript?","slug":"is-it-better-for-discourse-to-use-javascript-or-coffeescript","posts_count":56}],"user":{"user_option":{},"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png","name":"Robin Ward","email":"robin.ward@gmail.com","last_posted_at":"2015-05-07T15:23:35.074Z","last_seen_at":"2015-05-13T14:34:23.188Z","bio_raw":"Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.","bio_cooked":"

    Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.

    ","created_at":"2013-02-03T15:19:22.704Z","website":"http://eviltrout.com","location":"Toronto","can_edit":false,"can_edit_username":true,"can_edit_email":true,"can_edit_name":true,"stats":[{"action_type":13,"count":342,"id":null},{"action_type":12,"count":109,"id":null},{"action_type":4,"count":27,"id":null},{"action_type":5,"count":1607,"id":null},{"action_type":6,"count":771,"id":null},{"action_type":1,"count":333,"id":null},{"action_type":2,"count":2671,"id":null},{"action_type":7,"count":949,"id":null},{"action_type":9,"count":42,"id":null},{"action_type":3,"count":8,"id":null},{"action_type":11,"count":20,"id":null}],"can_send_private_messages":true,"can_send_private_message_to_user":false,"bio_excerpt":"Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.","trust_level":4,"moderator":true,"admin":true,"title":"co-founder","badge_count":23,"notification_count":3244,"has_title_badges":true,"custom_fields":{},"user_fields":{"1":"33"},"pending_count":0,"post_count":1987,"can_be_deleted":false,"can_delete_all_posts":false,"locale":"","email_digests":true,"email_private_messages":true,"email_direct":true,"email_always":true,"digest_after_days":7,"mailing_list_mode":false,"auto_track_topics_after_msecs":60000,"new_topic_duration_minutes":1440,"external_links_in_new_tab":false,"dynamic_favicon":true,"enable_quoting":true,"muted_category_ids":[],"tracked_category_ids":[],"watched_category_ids":[3],"private_messages_stats":{"all":101,"mine":13,"unread":3},"disable_jump_reply":false,"gravatar_avatar_upload_id":5275,"custom_avatar_upload_id":1573,"card_image_badge":"https://meta-discourse.global.ssl.fastly.net/uploads/default/36220/15b19c80dd99d5a5.png","card_image_badge_id":120,"muted_usernames":[],"invited_by":{"id":1,"username":"sam","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/sam/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"},"custom_groups":[{"id":44,"automatic":false,"name":"ubuntu","user_count":11,"alias_level":0,"visible":true,"automatic_membership_email_domains":null,"automatic_membership_retroactive":false,"primary_group":false,"title":null},{"id":47,"automatic":false,"name":"discourse","user_count":7,"alias_level":0,"visible":true,"automatic_membership_email_domains":null,"automatic_membership_retroactive":false,"primary_group":false,"title":null}],"featured_user_badge_ids":[5870,40673,5868],"card_badge":{"id":120,"name":"Garbage Man","description":"This Discourse developer successfully called something \"garbage!\"","grant_count":3,"allow_title":false,"multiple_grant":false,"icon":"https://meta-discourse.global.ssl.fastly.net/uploads/default/36220/15b19c80dd99d5a5.png","image":"https://meta-discourse.global.ssl.fastly.net/uploads/default/36220/15b19c80dd99d5a5.png","listable":false,"enabled":false,"badge_grouping_id":8,"system":false,"badge_type_id":3}}}, "/user_actions.json": {"user_actions":[{"action_type":7,"created_at":"2014-01-16T14:13:05Z","excerpt":"So again, \n\nWhat is the problem?\n\nI need to check user_trust_level , i get the 'username' from a form via ajax, i need to check what level he is on discourse \n\nAlso, if possible, i would like to get other details as well, like email address etc. \n\nI took a look at : https://github.com/discourse/dis…","avatar_template":"//www.gravatar.com/avatar/bdab7e61b3191e483492fd680f563fed.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/bdab7e61b3191e483492fd680f563fed.png?s={size}&r=pg&d=identicon","slug":"how-to-check-the-user-level-via-ajax","topic_id":11993,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":1,"reply_to_post_number":null,"username":"Abhishek_Gupta","name":"Abhishek Gupta","user_id":8021,"acting_username":"Abhishek_Gupta","acting_name":"Abhishek Gupta","acting_user_id":8021,"title":"How to check the user level via ajax?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-15T16:53:49Z","excerpt":"A good fix would be to have the ERB template do an if statement. We'd happily accept a PR that did this if you feel up to it: \n\n <% if SiteSetting.logo_url.present? %>\n display logo html\n<% else %>\n display title html\n<% end %>","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2","topic_id":10911,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"/users/activate-account pulling blank logo instead of defaulting to h2","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-15T15:21:37Z","excerpt":"A good fix would be to have the ERB template do an if statement. We'd happily accept a PR that did this if you feel up to it: \n\n <% if SiteSetting.logo_url.present? %>\n display logo html\n<% else %>\n display title html\n<% end %>","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2","topic_id":10911,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"/users/activate-account pulling blank logo instead of defaulting to h2","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-15T12:22:12Z","excerpt":"OK - i see what you mean. From the piwik code I should add: \n\n_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);\n\n? \n\nUnfortunately I have had to give up on Piwik for now because I have switched the forum to SSL on a free cert and have used up the free subdomain for the forum. …","avatar_template":"//localhost:3000/uploads/default/avatars/2a8/a3c/8fddcac642/{size}.jpg","acting_avatar_template":"//localhost:3000/uploads/default/avatars/2a8/a3c/8fddcac642/{size}.jpg","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":26,"reply_to_post_number":25,"username":"citkane","name":"Michael Jonker","user_id":7604,"acting_username":"citkane","acting_name":"Michael Jonker","acting_user_id":7604,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-15T11:16:36Z","excerpt":"@eviltrout recently added support for multiple API keys [wink] \n\n[]","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"allow-for-multiple-api-keys","topic_id":7444,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":null,"username":"zogstrip","name":"Régis Hanol","user_id":1995,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Allow for multiple API Keys","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-15T10:58:46Z","excerpt":"@eviltrout added a tooltip when you click on the user's avatar which allows you to show the posts made by that user \n\n[image]","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"to-group-posts-by-a-user","topic_id":7412,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":4,"reply_to_post_number":3,"username":"zogstrip","name":"Régis Hanol","user_id":1995,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"To group posts by a user","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-15T10:36:15Z","excerpt":"@eviltrout implemented per-user API key a while ago [wink] \n\n [image]\nTopics_-_Discourse_Meta-5.png884x339 29.6 KB\n","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"auth-using-rest-api","topic_id":5937,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"zogstrip","name":"Régis Hanol","user_id":1995,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Auth using REST API?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-15T09:55:17Z","excerpt":"@eviltrout has recently introduced this feature and has even blogged about it: \n\n \n \n \n \n eviltrout.com\n \n \n \n \n \n Hiding Offscreen Content in Ember.js - Evil Trout's Blog","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"infinite-scrolling-reusing-dom-nodes","topic_id":5186,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":null,"username":"zogstrip","name":"Régis Hanol","user_id":1995,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Infinite scrolling: Reusing DOM nodes","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-15T00:54:32Z","excerpt":"You can retrieve a user's JSON by making a call to /users/username.json but that assumes you know the user's username. If that's impossible, I would be happy to accept a PR that would return the current user JSON from /session/current-user or something like that. \n\nAdditionally, if you're looking to…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"watchmanmonitor","acting_name":"Watchman Monitoring","acting_user_id":8085,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T21:59:51Z","excerpt":"You can retrieve a user's JSON by making a call to /users/username.json but that assumes you know the user's username. If that's impossible, I would be happy to accept a PR that would return the current user JSON from /session/current-user or something like that. \n\nAdditionally, if you're looking to…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/9cfd2536afac32d209335b092094c12c.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"znation","acting_name":"znation","acting_user_id":8163,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T21:46:50Z","excerpt":"Okay I've fixed the https [point_right] http links on the server side and in the Javascript click tracking as @BhaelOchon pointed out. \n\nLet me know if you find anything else broken.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"broken-links-possibly-related-to-https","topic_id":11831,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Broken links, possibly related to HTTPS","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-14T21:43:28Z","excerpt":"Thanks for your help @eviltrout! I will consider making that change and sending a pull request. I may not get to it for a while. \n\nI am embedding Discourse on another site and it is mostly going well. I have indeed been using your blog for inspiration.","avatar_template":"//www.gravatar.com/avatar/9cfd2536afac32d209335b092094c12c.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/9cfd2536afac32d209335b092094c12c.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"znation","name":"znation","user_id":8163,"acting_username":"znation","acting_name":"znation","acting_user_id":8163,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T21:21:52Z","excerpt":"Okay I've fixed the https [point_right] http links on the server side and in the Javascript click tracking as @BhaelOchon pointed out. \n\nLet me know if you find anything else broken.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"broken-links-possibly-related-to-https","topic_id":11831,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Broken links, possibly related to HTTPS","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T21:03:07Z","excerpt":"Okay I've fixed the https [point_right] http links on the server side and in the Javascript click tracking as @BhaelOchon pointed out. \n\nLet me know if you find anything else broken.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"broken-links-possibly-related-to-https","topic_id":11831,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Broken links, possibly related to HTTPS","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T20:42:51Z","excerpt":"You can retrieve a user's JSON by making a call to /users/username.json but that assumes you know the user's username. If that's impossible, I would be happy to accept a PR that would return the current user JSON from /session/current-user or something like that. \n\nAdditionally, if you're looking to…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T20:29:23Z","excerpt":"You can retrieve a user's JSON by making a call to /users/username.json but that assumes you know the user's username. If that's impossible, I would be happy to accept a PR that would return the current user JSON from /session/current-user or something like that. \n\nAdditionally, if you're looking to…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T19:20:28Z","excerpt":"Perhaps the ['trackpageView'] is not the correct API call? We can probably send more information across such as the URL.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":25,"reply_to_post_number":24,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T19:19:46Z","excerpt":"Nope but I bet you can find one!","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"how-far-to-take-user-documentation","topic_id":11943,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":4,"reply_to_post_number":3,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"How far to take user documentation?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-14T18:37:05Z","excerpt":"I'd be glad to write a pull request to take use there. Is there a specific part of their documentation you have in mind?","avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","slug":"how-far-to-take-user-documentation","topic_id":11943,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"watchmanmonitor","name":"Watchman Monitoring","user_id":8085,"acting_username":"watchmanmonitor","acting_name":"Watchman Monitoring","acting_user_id":8085,"title":"How far to take user documentation?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-14T16:04:28Z","excerpt":"Thanks @eviltrout , the code in the 'bottom of pages' now reads: \n\n<script type="text/javascript">\nDiscourse.PageTracker.current().on('change', function() {\n console.log('tracked!')\n _paq.push(['trackPageView']);\n});\n</script>\n\nThe console is logging 'tracked!' and piwik is logging for each page c…","avatar_template":"//localhost:3000/uploads/default/avatars/2a8/a3c/8fddcac642/{size}.jpg","acting_avatar_template":"//localhost:3000/uploads/default/avatars/2a8/a3c/8fddcac642/{size}.jpg","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":23,"reply_to_post_number":22,"username":"citkane","name":"Michael Jonker","user_id":7604,"acting_username":"citkane","acting_name":"Michael Jonker","acting_user_id":7604,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T15:58:27Z","excerpt":"This topic is now archived. It is frozen and cannot be changed in any way.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"regression-cannot-sort-topic-list","topic_id":11944,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":4,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Regression: Cannot sort topic list","deleted":false,"hidden":false,"moderator_action":true,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T15:26:57Z","excerpt":"I do think that leading them into the official rails documentation at that point is not a bad idea. Like "congratulations, everything is ready but now you'll need to understand the platform we built it in to be productive."","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"how-far-to-take-user-documentation","topic_id":11943,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"How far to take user documentation?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T08:28:00Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-14T00:21:26Z","excerpt":"In pull request 1821, @eviltrout asked: \n\n "About rails s: I wouldn't be against adding it but at what point do we stop holding their hand and expect them to know how rails works? I'm sure rails documentation could do a better job than us. Actually maybe we should just link to that? \n\nWhat point to …","avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","slug":"how-far-to-take-user-documentation","topic_id":11943,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":1,"reply_to_post_number":null,"username":"watchmanmonitor","name":"Watchman Monitoring","user_id":8085,"acting_username":"watchmanmonitor","acting_name":"Watchman Monitoring","acting_user_id":8085,"title":"How far to take user documentation?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-13T21:58:28Z","excerpt":"It looks uneeded, but you need to review a fair amount of code to confirm it is not needed. \n\nI am going to keep it for now cause its safer under some weird edge conditions.","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"ruby-question-about-use-of-klass-self-in-the-site-customization-rb","topic_id":11889,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Ruby question about use of klass=self in the site_customization.rb","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T21:11:32Z","excerpt":"I had to fix an issue with Google analytics so I added a new API hook that can be used. \n\nIf you add the following it should work: \n\n Discourse.PageTracker.current().on('change', function() {\n _paq.push(['trackPageView']);\n});","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-13T21:10:57Z","excerpt":"Having a look, the fix is a bit scary imho, we should fix the root issue.","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"error-after-update-to-0-9-8-1","topic_id":11903,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":11,"reply_to_post_number":10,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Error after update to 0.9.8.1","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T20:50:34Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//localhost:3000/uploads/default/avatars/527/614/d16e1504d9/{size}.jpg","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"trident","acting_name":"Ben T","acting_user_id":5707,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T20:44:56Z","excerpt":"I had to fix an issue with Google analytics so I added a new API hook that can be used. \n\nIf you add the following it should work: \n\n Discourse.PageTracker.current().on('change', function() {\n _paq.push(['trackPageView']);\n});","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T20:40:21Z","excerpt":"I had to fix an issue with Google analytics so I added a new API hook that can be used. \n\nIf you add the following it should work: \n\n Discourse.PageTracker.current().on('change', function() {\n _paq.push(['trackPageView']);\n});","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T19:52:04Z","excerpt":"@Sam do you have any idea why only some people are getting this issue? I dont' mind the proposed fix but I'd prefer to know why it happens in the first place.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"error-after-update-to-0-9-8-1","topic_id":11903,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":10,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Error after update to 0.9.8.1","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T19:01:19Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T18:50:14Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T18:47:33Z","excerpt":"I am pretty sure that the denizens of SO are correct and the variable is unneeded. @sam can confirm but it seems like it was once needed for something that has since been removed and the variable declaration was left intact.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"ruby-question-about-use-of-klass-self-in-the-site-customization-rb","topic_id":11889,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Ruby question about use of klass=self in the site_customization.rb","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T18:45:41Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T17:19:08Z","excerpt":"@Sam do you have any idea why only some people are getting this issue? I dont' mind the proposed fix but I'd prefer to know why it happens in the first place.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/5120fc4e345db0d1a964888272073819.png?s={size}&r=pg&d=identicon","slug":"error-after-update-to-0-9-8-1","topic_id":11903,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":10,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"riking","acting_name":"Kane York","acting_user_id":6626,"title":"Error after update to 0.9.8.1","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-13T16:41:31Z","excerpt":"I'd love to see API support. @sam and @eviltrout, I can facilitate an intro to the piwik guys if you want—I've written about them before and they're typically super-responsive. Because I know you guys are totally hunting for new stuff to do [wink]","avatar_template":"//localhost:3000/uploads/default/avatars/95a/06d/c337428568/{size}.png","acting_avatar_template":"//localhost:3000/uploads/default/avatars/95a/06d/c337428568/{size}.png","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":20,"reply_to_post_number":null,"username":"Lee_Ars","name":"Lee_Ars","user_id":4457,"acting_username":"Lee_Ars","acting_name":"Lee_Ars","acting_user_id":4457,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T16:15:51Z","excerpt":"The code looks okay but it's hard to debug this way. \n\nOne thing you could do is add a: console.log('tracked!') just before line 8. Then open a developer console and see if the javascript is running properly.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T15:10:41Z","excerpt":"This is really interesting. I'd like to hear your findings.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"focus-events-track-which-window-is-the-last-active-instance-of-a-forum-edit","topic_id":11872,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":9,"reply_to_post_number":8,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Focus events: Track which window is the last active instance of a forum Edit","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T15:02:45Z","excerpt":"The code looks okay but it's hard to debug this way. \n\nOne thing you could do is add a: console.log('tracked!') just before line 8. Then open a developer console and see if the javascript is running properly.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T14:53:13Z","excerpt":"@Sam do you have any idea why only some people are getting this issue? I dont' mind the proposed fix but I'd prefer to know why it happens in the first place.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"error-after-update-to-0-9-8-1","topic_id":11903,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":10,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Error after update to 0.9.8.1","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-13T06:27:26Z","excerpt":"Can this be archived @eviltrout?","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"search-not-working-for-staff-users","topic_id":11371,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":13,"reply_to_post_number":null,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Search not working for Staff users","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-13T05:32:46Z","excerpt":"When you navigate to another topic using the "suggested topics" area we are not registering a page view with Google. \n\n@eviltrout perhaps we should do this from discourse location instead of application controller?","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"google-analytics-is-not-registering-page-views","topic_id":11914,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":1,"reply_to_post_number":null,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Google analytics is not registering page views","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-13T02:50:25Z","excerpt":"@eviltrout any ideas here, the code seems correct","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":17,"reply_to_post_number":16,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-12T22:31:35Z","excerpt":"This is an interesting approach an an interesting feature. @eviltrout your thoughts. Essentially allows us to have notifications cross tabs.","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"focus-events-track-which-window-is-the-last-active-instance-of-a-forum-edit","topic_id":11872,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":4,"reply_to_post_number":1,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Focus events: Track which window is the last active instance of a forum Edit","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-12T18:01:04Z","excerpt":"This was the link \n\nmetric_fu \n\n[metric_fu](https://github.com/metricfu/metric_fu/blob/b1bf8feb921916fc265f041efa3157a6a6530a9b/lib/metric_fu/logging/mf_debugger.rb#L24)\n\nSeems to work fine now that @eviltrout worked so hard to get us MDTest 1.1 compliant.","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"underscores-in-linked-text-can-cause-markdown-bug","topic_id":10848,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":null,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Underscores in linked text can cause markdown bug","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-12T04:14:06Z","excerpt":"Awesome plugin, but doesn't seem to work out of the box with images \n\nhttps://github.com/discourse/discourse-spoiler-alert/issues/2","avatar_template":"//localhost:3000/uploads/default/avatars/276/f19/3826efe463/{size}.jpg","acting_avatar_template":"//localhost:3000/uploads/default/avatars/276/f19/3826efe463/{size}.jpg","slug":"brand-new-plugin-interface","topic_id":8793,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":64,"reply_to_post_number":44,"username":"xrvk","name":"Eero Heikkinen","user_id":8068,"acting_username":"xrvk","acting_name":"Eero Heikkinen","acting_user_id":8068,"title":"Brand new plugin interface","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-11T23:36:11Z","excerpt":"A few things, \n\n@eviltrout myself and many others have discourse_docker hosted on DigitalOcean, my user cpu is usually around 2% I have plenty of capacity. \n\nI know that stonehearth and other larger scale discourse work on DigitalOcean fine. Officially we strongly recommend a 2GB instance, thoug…","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"performance-issue-on-digital-ocean-with-discourse-docker","topic_id":11895,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Performance issue on DigitalOcean with discourse_docker","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-11T00:58:23Z","excerpt":"Confirmed on try.discourse.org, this is still an issue. \n\n@eviltrout can you add that to your list -- unless you are a staff member you should not be able to delete (your own) posts from an archived topic.","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"archived-discussions-still-allow-posts-to-be-deleted","topic_id":6479,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":null,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Archived discussions still allow posts to be deleted","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-11T00:35:38Z","excerpt":"Agree, @eviltrout can you make sure the usercard is using the same logic as the user page in displaying profile info?","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"usercard-does-not-resize-for-obnoxiously-large-images","topic_id":11007,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":5,"reply_to_post_number":4,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Usercard does not resize for obnoxiously large images","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-11T00:34:06Z","excerpt":"@eviltrout can you make sure the "import post" button is suppressed on the user page when editing "about me"? \n\n(I agree it is like a "lose all my work" button on that page if you happen to press it..) \n\nThen I can archive this.","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"quote-post-button-should-be-disabled-or-raise-an-error-when-creating-a-new-topic","topic_id":834,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":5,"reply_to_post_number":4,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"\"Quote Post\" button should be disabled or raise an error when creating a new topic","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-10T21:00:11Z","excerpt":">\n\nLooks good now. Thanks for these fixes @eviltrout, we (and markdown-js) are now MDTest 1.1 compliant!","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"text-editor-issue-with-the-code-block","topic_id":10050,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":5,"reply_to_post_number":null,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Text Editor issue with the code block","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":1,"created_at":"2014-01-10T20:07:46Z","excerpt":"We can't repro that one, also seems a bit obscure. But thank you very much for all the reports, whenever I see a bug entry from YOU I always know it is going to be a good one based on experience here and elsewhere. [trophy]","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"security-error-on-console-noticed-on-meta","topic_id":11825,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":12,"reply_to_post_number":11,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Security Error on console (noticed on meta)","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T19:48:08Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T19:47:17Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/42776c4982dff1fa45ee8248532f8ad0.png?s={size}&r=pg&d=identicon","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"neil","acting_name":"Neil","acting_user_id":2,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T17:39:24Z","excerpt":"We should consider doing what Google Drive does: they intercept cmd-f and pop up a box that allows you to dynamically search.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/5120fc4e345db0d1a964888272073819.png?s={size}&r=pg&d=identicon","slug":"ctrl-f-search-is-interrupted-by-quotation-popup","topic_id":7114,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":12,"reply_to_post_number":11,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"riking","acting_name":"Kane York","acting_user_id":6626,"title":"Ctrl+F search is interrupted by quotation popup","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T17:29:15Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/5120fc4e345db0d1a964888272073819.png?s={size}&r=pg&d=identicon","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"riking","acting_name":"Kane York","acting_user_id":6626,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T17:24:37Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-10T17:02:35Z","excerpt":"Fixed [smile] \n\ntop - 12:02:00 up 12 days, 2:16, 1 user, load average: 0.28, 0.92, 0.97\nTasks: 115 total, 1 running, 114 sleeping, 0 stopped, 0 zombie\nCpu0 : 0.7%us, 0.3%sy, 0.0%ni, 99.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st\nCpu1 : 0.7%us, 0.3%sy, 0.0%ni, 99.0%id, 0.0%wa, 0.0%hi,…","avatar_template":"//localhost:3000/uploads/default/avatars/886/ea8/e533d87fd9/{size}.png","acting_avatar_template":"//localhost:3000/uploads/default/avatars/886/ea8/e533d87fd9/{size}.png","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":23,"reply_to_post_number":22,"username":"michaeld","name":"Michael","user_id":6548,"acting_username":"michaeld","acting_name":"Michael","acting_user_id":6548,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T16:58:12Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//localhost:3000/uploads/default/avatars/527/614/d16e1504d9/{size}.jpg","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"trident","acting_name":"Ben T","acting_user_id":5707,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null}]}, "/topics/created-by/eviltrout.json": {"users":[{"id":19,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon"},{"id":5460,"username":"ned","avatar_template":"//localhost:3000/uploads/default/avatars/06b/90d/3b3ea7e56b/{size}.png"},{"id":402,"username":"thebrianbarlow","avatar_template":"//www.gravatar.com/avatar/5ddf2459e8edd6cf52dfff6cb41ca70d.png?s={size}&r=pg&d=identicon"},{"id":5707,"username":"trident","avatar_template":"//localhost:3000/uploads/default/avatars/527/614/d16e1504d9/{size}.jpg"},{"id":32,"username":"codinghorror","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon"},{"id":1995,"username":"zogstrip","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon"},{"id":2702,"username":"ryanflorence","avatar_template":"//www.gravatar.com/avatar/749001c9fe6927c4b069a45c2a3d68f7.png?s={size}&r=pg&d=identicon"},{"id":9,"username":"tms","avatar_template":"//www.gravatar.com/avatar/3981cd271c302f5cba628c6b6d2b32ee.png?s={size}&r=pg&d=identicon"},{"id":1,"username":"sam","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon"},{"id":2636,"username":"lonnon","avatar_template":"//www.gravatar.com/avatar/9489ef302fbff6c19bba507d09f8cd1d.png?s={size}&r=pg&d=identicon"}],"topic_list":{"can_create_topic":false,"draft":null,"draft_key":"new_topic","draft_sequence":null,"topics":[{"id":7764,"title":"New: Reply via Email Support!","fancy_title":"New: Reply via Email Support!","slug":"new-reply-via-email-support","posts_count":32,"reply_count":24,"highest_post_number":35,"image_url":"/uploads/meta_discourse/1227/8f4e5818dfaa56c7.png","created_at":"2013-06-25T11:58:39.000-04:00","last_posted_at":"2014-01-09T18:53:06.000-05:00","bumped":true,"bumped_at":"2014-01-09T17:09:40.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":2201,"like_count":46,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":5460},{"extras":null,"description":"Frequent Poster","user_id":402},{"extras":null,"description":"Frequent Poster","user_id":5707},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":9318,"title":"Discourse has a new Markdown Parser!","fancy_title":"Discourse has a new Markdown Parser!","slug":"discourse-has-a-new-markdown-parser","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2013-08-24T14:08:06.000-04:00","last_posted_at":"2013-08-24T14:08:06.000-04:00","bumped":true,"bumped_at":"2013-08-24T14:13:25.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":812,"like_count":13,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":19}]},{"id":7019,"title":"Discourse Ember Refactorings","fancy_title":"Discourse Ember Refactorings","slug":"discourse-ember-refactorings","posts_count":5,"reply_count":3,"highest_post_number":5,"image_url":null,"created_at":"2013-05-30T11:16:36.000-04:00","last_posted_at":"2013-06-02T11:22:58.000-04:00","bumped":true,"bumped_at":"2013-06-02T11:22:58.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":1075,"like_count":15,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":1995},{"extras":null,"description":"Frequent Poster","user_id":2702}]},{"id":4650,"title":"Migrating off Active Record Observers","fancy_title":"Migrating off Active Record Observers","slug":"migrating-off-active-record-observers","posts_count":8,"reply_count":7,"highest_post_number":8,"image_url":null,"created_at":"2013-03-11T11:26:13.000-04:00","last_posted_at":"2013-05-14T18:40:16.000-04:00","bumped":true,"bumped_at":"2013-05-14T18:40:16.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":377,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":9},{"extras":null,"description":"Frequent Poster","user_id":1995},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":4960,"title":"Vagrant Updates!","fancy_title":"Vagrant Updates!","slug":"vagrant-updates","posts_count":5,"reply_count":3,"highest_post_number":5,"image_url":"/plugins/emoji/images/fish.png","created_at":"2013-03-20T22:29:22.000-04:00","last_posted_at":"2013-03-21T19:06:40.000-04:00","bumped":true,"bumped_at":"2013-03-21T19:06:40.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":500,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":1995}]},{"id":2918,"title":"New: Updated Docs","fancy_title":"New: Updated Docs","slug":"new-updated-docs","posts_count":3,"reply_count":2,"highest_post_number":3,"image_url":null,"created_at":"2013-02-12T12:13:02.000-05:00","last_posted_at":"2013-02-15T17:57:19.000-05:00","bumped":true,"bumped_at":"2013-02-15T17:57:19.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":457,"like_count":10,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":10,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":2636}]}]}} }; From 2f926cfdd3d40cfc414a5a82a3319f62c6aa83c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 17 Feb 2016 11:02:44 +0100 Subject: [PATCH 098/140] only drop users table columns if they exists --- db/fixtures/009_users.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index 4c5f118754..a75b81b565 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -55,7 +55,7 @@ if User.exec_sql("SELECT 1 FROM schema_migration_details automatically_unpin_topics digest_after_days ].each do |column| - User.exec_sql("ALTER TABLE users DROP column #{column}") + User.exec_sql("ALTER TABLE users DROP column IF EXISTS #{column}") end end From 8893d711e0214fee3272bcc3bdf01bbd74357c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 17 Feb 2016 11:25:49 +0100 Subject: [PATCH 099/140] FEATURE: new pop3 polling configuration admin dashboard check --- app/models/admin_dashboard_data.rb | 7 ++++++- lib/validators/pop3_polling_enabled_setting_validator.rb | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 5cfa8bea2c..e2a130053a 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -62,7 +62,8 @@ class AdminDashboardData :failing_emails_check, :default_logo_check, :contact_email_check, :send_consumer_email_check, :title_check, :site_description_check, :site_contact_username_check, - :notification_email_check, :subfolder_ends_in_slash_check + :notification_email_check, :subfolder_ends_in_slash_check, + :pop3_polling_configuration add_problem_check do sidekiq_check || queue_size_check @@ -205,4 +206,8 @@ class AdminDashboardData I18n.t('dashboard.subfolder_ends_in_slash') if Discourse.base_uri =~ /\/$/ end + def pop3_polling_configuration + POP3PollingEnabledSettingValidator.new.error_message if SiteSetting.pop3_polling_enabled + end + end diff --git a/lib/validators/pop3_polling_enabled_setting_validator.rb b/lib/validators/pop3_polling_enabled_setting_validator.rb index adee3dfc5d..f62e252bbb 100644 --- a/lib/validators/pop3_polling_enabled_setting_validator.rb +++ b/lib/validators/pop3_polling_enabled_setting_validator.rb @@ -23,7 +23,7 @@ class POP3PollingEnabledSettingValidator I18n.t("site_settings.errors.pop3_polling_username_is_empty") elsif SiteSetting.pop3_polling_password.blank? I18n.t("site_settings.errors.pop3_polling_password_is_empty") - else + elsif !authentication_works? I18n.t("site_settings.errors.pop3_polling_authentication_failed") end end From 532fb7ea9dbc5872460f77b38a32a8420d79b210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 17 Feb 2016 11:57:06 +0100 Subject: [PATCH 100/140] fix smoke tests --- db/fixtures/009_users.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index a75b81b565..ccdcaceace 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -70,20 +70,18 @@ if ENV["SMOKE"] == "1" u.username_lower = "smoke_user" u.email = "smoke_user@discourse.org" u.password = "P4ssw0rd" - u.email_direct = false - u.email_digests = false - u.email_private_messages = false u.active = true u.approved = true u.approved_at = Time.now u.trust_level = TrustLevel[3] end.first - EmailToken.seed do |et| - et.id = 1 - et.user_id = smoke_user.id - et.email = smoke_user.email - et.confirmed = true - end + UserOption.where(user_id: smoke_user.id).update_all( + email_direct: false, + email_digests: false, + email_private_messages: false, + ) + + EmailToken.where(user_id: smoke_user.id).update_all(confirmed: true) end From 52a66826907bebe81b13ea6bd417f13a05443507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 17 Feb 2016 17:31:46 +0100 Subject: [PATCH 101/140] FIX: don't create an EmailLog when we can't send a digest --- lib/email/sender.rb | 4 ++++ spec/components/email/sender_spec.rb | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/email/sender.rb b/lib/email/sender.rb index f0cef9ff22..6168b086df 100644 --- a/lib/email/sender.rb +++ b/lib/email/sender.rb @@ -23,6 +23,10 @@ module Email def send return if SiteSetting.disable_emails + + return if ActionMailer::Base::NullMail === @message + return if ActionMailer::Base::NullMail === (@message.message rescue nil) + return skip(I18n.t('email_log.message_blank')) if @message.blank? return skip(I18n.t('email_log.message_to_blank')) if @message.to.blank? diff --git a/spec/components/email/sender_spec.rb b/spec/components/email/sender_spec.rb index b77844e01d..1fc6d71ad1 100644 --- a/spec/components/email/sender_spec.rb +++ b/spec/components/email/sender_spec.rb @@ -7,7 +7,13 @@ describe Email::Sender do SiteSetting.expects(:disable_emails).returns(true) Mail::Message.any_instance.expects(:deliver_now).never message = Mail::Message.new(to: "hello@world.com" , body: "hello") - Email::Sender.new(message, :hello).send + expect(Email::Sender.new(message, :hello).send).to eq(nil) + end + + it "doesn't deliver mail when the message is of type NullMail" do + Mail::Message.any_instance.expects(:deliver_now).never + message = ActionMailer::Base::NullMail.new + expect(Email::Sender.new(message, :hello).send).to eq(nil) end it "doesn't deliver mail when the message is nil" do From f9c5cded6f3b5bc4eaa9e3460afb09fc07eacd46 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 18 Feb 2016 13:20:22 +1100 Subject: [PATCH 102/140] Correct live refresh routine for notifications --- .../subscribe-user-notifications.js.es6 | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 index 5923dfca87..fd820d8202 100644 --- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 @@ -49,34 +49,35 @@ export default { const oldNotifications = stale.results.get('content'); const staleIndex = _.findIndex(oldNotifications, {id: lastNotification.id}); - if (staleIndex > -1) { - oldNotifications.splice(staleIndex, 1); + if (staleIndex === -1) { + // this gets a bit tricky, uread pms are bumped to front + var insertPosition = 0; + if (lastNotification.notification_type !== 6) { + insertPosition = _.findIndex(oldNotifications, function(n){ + return n.notification_type !== 6 || n.read; + }); + insertPosition = insertPosition === -1 ? oldNotifications.length - 1 : insertPosition; + } + + oldNotifications.insertAt(insertPosition, Em.Object.create(lastNotification)); } - // this gets a bit tricky, uread pms are bumped to front - var insertPosition = 0; - if (lastNotification.notification_type !== 6) { - insertPosition = _.findIndex(oldNotifications, function(n){ - return n.notification_type !== 6 || n.read; - }); - insertPosition = insertPosition === -1 ? oldNotifications.length - 1 : insertPosition; - } + for (var idx=0; idx < data.recent.length; idx++) { + var old; + while(old = oldNotifications[idx]) { + var info = data.recent[idx]; - oldNotifications.splice(insertPosition, 0, Em.Object.create(lastNotification)); - - var idx=0; - data.recent.forEach((info)=> { - var old = oldNotifications[idx]; - if (old) { if (old.get('id') !== info[0]) { - oldNotifications.splice(idx, 1); - return; - } else if (old.get('read') !== info[1]) { - old.set('read', info[1]); + oldNotifications.removeAt(idx); + } else { + if (old.get('read') !== info[1]) { + old.set('read', info[1]); + } + break; } } - idx += 1; - }); + if ( !old ) { break; } + } } }, user.notification_channel_position); From f0e942f6474ffa41be835e2deda9ebdac2f6c93c Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 18 Feb 2016 16:57:22 +1100 Subject: [PATCH 103/140] PERF: move 3 more option columns out of the user table --- .../javascripts/discourse/models/user.js.es6 | 6 +- .../discourse/templates/user/preferences.hbs | 4 +- app/jobs/regular/update_top_redirection.rb | 2 +- app/models/topic_tracking_state.rb | 7 +- app/models/topic_user.rb | 55 +++++------ app/models/user.rb | 85 ----------------- app/models/user_option.rb | 71 ++++++++++++++ app/serializers/current_user_serializer.rb | 10 +- app/serializers/listable_topic_serializer.rb | 2 +- app/serializers/user_option_serializer.rb | 13 ++- app/serializers/user_serializer.rb | 10 -- app/services/user_updater.rb | 12 +-- db/fixtures/009_users.rb | 9 +- ...9_move_tracking_options_to_user_options.rb | 16 ++++ lib/topic_query.rb | 2 +- spec/components/topic_query_spec.rb | 12 +-- spec/fabricators/user_option_fabricator.rb | 2 + spec/models/topic_user_spec.rb | 13 ++- spec/models/user_option_spec.rb | 92 ++++++++++++++++++ spec/models/user_spec.rb | 94 +------------------ spec/serializers/user_serializer_spec.rb | 4 + spec/services/user_updater_spec.rb | 7 +- 22 files changed, 278 insertions(+), 250 deletions(-) create mode 100644 db/migrate/20160225050319_move_tracking_options_to_user_options.rb create mode 100644 spec/fabricators/user_option_fabricator.rb create mode 100644 spec/models/user_option_spec.rb diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 516f7b1f7a..c57574431e 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -141,13 +141,11 @@ const User = RestModel.extend({ save() { const data = this.getProperties( - 'auto_track_topics_after_msecs', 'bio_raw', 'website', 'location', 'name', 'locale', - 'new_topic_duration_minutes', 'custom_fields', 'user_fields', 'muted_usernames', @@ -165,7 +163,9 @@ const User = RestModel.extend({ 'enable_quoting', 'disable_jump_reply', 'automatically_unpin_topics', - 'digest_after_days' + 'digest_after_days', + 'new_topic_duration_minutes', + 'auto_track_topics_after_msecs' ].forEach(s => { data[s] = this.get(`user_option.${s}`); }); diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index 4c65524e82..06bab9cbf4 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -201,12 +201,12 @@
    - {{combo-box valueAttribute="value" content=considerNewTopicOptions value=model.new_topic_duration_minutes}} + {{combo-box valueAttribute="value" content=considerNewTopicOptions value=model.user_option.new_topic_duration_minutes}}
    - {{combo-box valueAttribute="value" content=autoTrackDurations value=model.auto_track_topics_after_msecs}} + {{combo-box valueAttribute="value" content=autoTrackDurations value=model.user_option.auto_track_topics_after_msecs}}
    {{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.user_option.external_links_in_new_tab}} diff --git a/app/jobs/regular/update_top_redirection.rb b/app/jobs/regular/update_top_redirection.rb index 496f86f589..776cdfca7d 100644 --- a/app/jobs/regular/update_top_redirection.rb +++ b/app/jobs/regular/update_top_redirection.rb @@ -4,7 +4,7 @@ module Jobs def execute(args) if user = User.find_by(id: args[:user_id]) - user.update_column(:last_redirected_to_top_at, args[:redirected_at]) + user.user_option.update_column(:last_redirected_to_top_at, args[:redirected_at]) end end end diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index 5f96c2e262..7ffc6ad1e1 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -104,9 +104,9 @@ class TopicTrackingState def self.treat_as_new_topic_clause User.where("GREATEST(CASE - WHEN COALESCE(u.new_topic_duration_minutes, :default_duration) = :always THEN u.created_at - WHEN COALESCE(u.new_topic_duration_minutes, :default_duration) = :last_visit THEN COALESCE(u.previous_visit_at,u.created_at) - ELSE (:now::timestamp - INTERVAL '1 MINUTE' * COALESCE(u.new_topic_duration_minutes, :default_duration)) + WHEN COALESCE(uo.new_topic_duration_minutes, :default_duration) = :always THEN u.created_at + WHEN COALESCE(uo.new_topic_duration_minutes, :default_duration) = :last_visit THEN COALESCE(u.previous_visit_at,u.created_at) + ELSE (:now::timestamp - INTERVAL '1 MINUTE' * COALESCE(uo.new_topic_duration_minutes, :default_duration)) END, us.new_since, :min_date)", now: DateTime.now, last_visit: User::NewTopicDuration::LAST_VISIT, @@ -169,6 +169,7 @@ class TopicTrackingState FROM topics JOIN users u on u.id = :user_id JOIN user_stats AS us ON us.user_id = u.id + JOIN user_options AS uo ON uo.user_id = u.id JOIN categories c ON c.id = topics.category_id LEFT JOIN topic_users tu ON tu.topic_id = topics.id AND tu.user_id = u.id WHERE u.id = :user_id AND diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 25298af1f4..1a50237824 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -104,7 +104,7 @@ class TopicUser < ActiveRecord::Base if rows == 0 now = DateTime.now - auto_track_after = User.select(:auto_track_topics_after_msecs).find_by(id: user_id).try(:auto_track_topics_after_msecs) + auto_track_after = UserOption.where(user_id: user_id).pluck(:auto_track_topics_after_msecs).first auto_track_after ||= SiteSetting.default_other_auto_track_topics_after_msecs if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed].to_i || 0) @@ -140,6 +140,31 @@ class TopicUser < ActiveRecord::Base # Update the last read and the last seen post count, but only if it doesn't exist. # This would be a lot easier if psql supported some kind of upsert + UPDATE_TOPIC_USER_SQL = "UPDATE topic_users + SET + last_read_post_number = GREATEST(:post_number, tu.last_read_post_number), + highest_seen_post_number = t.highest_post_number, + total_msecs_viewed = LEAST(tu.total_msecs_viewed + :msecs,86400000), + notification_level = + case when tu.notifications_reason_id is null and (tu.total_msecs_viewed + :msecs) > + coalesce(uo.auto_track_topics_after_msecs,:threshold) and + coalesce(uo.auto_track_topics_after_msecs, :threshold) >= 0 then + :tracking + else + tu.notification_level + end + FROM topic_users tu + join topics t on t.id = tu.topic_id + join users u on u.id = :user_id + join user_options uo on uo.user_id = :user_id + WHERE + tu.topic_id = topic_users.topic_id AND + tu.user_id = topic_users.user_id AND + tu.topic_id = :topic_id AND + tu.user_id = :user_id + RETURNING + topic_users.notification_level, tu.notification_level old_level, tu.last_read_post_number + " def update_last_read(user, topic_id, post_number, msecs, opts={}) return if post_number.blank? msecs = 0 if msecs.to_i < 0 @@ -160,31 +185,7 @@ class TopicUser < ActiveRecord::Base # ... user visited the topic but did not read the posts # # 86400000 = 1 day - rows = exec_sql("UPDATE topic_users - SET - last_read_post_number = GREATEST(:post_number, tu.last_read_post_number), - highest_seen_post_number = t.highest_post_number, - total_msecs_viewed = LEAST(tu.total_msecs_viewed + :msecs,86400000), - notification_level = - case when tu.notifications_reason_id is null and (tu.total_msecs_viewed + :msecs) > - coalesce(u.auto_track_topics_after_msecs,:threshold) and - coalesce(u.auto_track_topics_after_msecs, :threshold) >= 0 then - :tracking - else - tu.notification_level - end - FROM topic_users tu - join topics t on t.id = tu.topic_id - join users u on u.id = :user_id - WHERE - tu.topic_id = topic_users.topic_id AND - tu.user_id = topic_users.user_id AND - tu.topic_id = :topic_id AND - tu.user_id = :user_id - RETURNING - topic_users.notification_level, tu.notification_level old_level, tu.last_read_post_number - ", - args).values + rows = exec_sql(UPDATE_TOPIC_USER_SQL,args).values if rows.length == 1 before = rows[0][1].to_i @@ -206,7 +207,7 @@ class TopicUser < ActiveRecord::Base if rows.length == 0 # The user read at least one post in a topic that they haven't viewed before. args[:new_status] = notification_levels[:regular] - if (user.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs) == 0 + if (user.user_option.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs) == 0 args[:new_status] = notification_levels[:tracking] end TopicTrackingState.publish_read(topic_id, post_number, user.id, args[:new_status]) diff --git a/app/models/user.rb b/app/models/user.rb index 5634cbeaf3..71ee83831a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -77,8 +77,6 @@ class User < ActiveRecord::Base after_initialize :add_trust_level - before_create :set_default_user_preferences - after_create :create_email_token after_create :create_user_stat after_create :create_user_option @@ -90,7 +88,6 @@ class User < ActiveRecord::Base before_save :update_username_lower before_save :ensure_password_is_hashed - after_save :update_tracked_topics after_save :clear_global_notice_if_needed after_save :refresh_avatar after_save :badge_grant @@ -626,20 +623,6 @@ class User < ActiveRecord::Base Promotion.new(self).change_trust_level!(level, opts) end - def treat_as_new_topic_start_date - duration = new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes.to_i - times = [case duration - when User::NewTopicDuration::ALWAYS - created_at - when User::NewTopicDuration::LAST_VISIT - previous_visit_at || user_stat.new_since - else - duration.minutes.ago - end, user_stat.new_since, Time.at(SiteSetting.min_new_topics_time).to_datetime] - - times.max - end - def readable_name return "#{name} (#{username})" if name.present? && name != username username @@ -730,51 +713,6 @@ class User < ActiveRecord::Base .exists? end - def should_be_redirected_to_top - redirected_to_top.present? - end - - def redirected_to_top - # redirect is enabled - return unless SiteSetting.redirect_users_to_top_page - # top must be in the top_menu - return unless SiteSetting.top_menu =~ /(^|\|)top(\||$)/i - # not enough topics - return unless period = SiteSetting.min_redirected_to_top_period - - if !seen_before? || (trust_level == 0 && !redirected_to_top_yet?) - update_last_redirected_to_top! - return { - reason: I18n.t('redirected_to_top_reasons.new_user'), - period: period - } - elsif last_seen_at < 1.month.ago - update_last_redirected_to_top! - return { - reason: I18n.t('redirected_to_top_reasons.not_seen_in_a_month'), - period: period - } - end - - # don't redirect to top - nil - end - - def redirected_to_top_yet? - last_redirected_to_top_at.present? - end - - def update_last_redirected_to_top! - key = "user:#{id}:update_last_redirected_to_top" - delay = SiteSetting.active_user_rate_limit_secs - - # only update last_redirected_to_top_at once every minute - return unless $redis.setnx(key, "1") - $redis.expire(key, delay) - - # delay the update - Jobs.enqueue_in(delay / 2, :update_top_redirection, user_id: self.id, redirected_at: Time.zone.now) - end def refresh_avatar return if @import_mode @@ -880,11 +818,6 @@ class User < ActiveRecord::Base end end - def update_tracked_topics - return unless auto_track_topics_after_msecs_changed? - TrackedTopicsUpdater.new(id, auto_track_topics_after_msecs).call - end - def clear_global_notice_if_needed if admin && SiteSetting.has_login_hint SiteSetting.has_login_hint = false @@ -970,14 +903,6 @@ class User < ActiveRecord::Base end end - def set_default_user_preferences - set_default_other_new_topic_duration_minutes - set_default_other_auto_track_topics_after_msecs - - # needed, otherwise the callback chain is broken... - true - end - def set_default_categories_preferences values = [] @@ -1025,12 +950,6 @@ class User < ActiveRecord::Base end - %w{new_topic_duration_minutes auto_track_topics_after_msecs}.each do |s| - define_method("set_default_other_#{s}") do - self.send("#{s}=", SiteSetting.send("default_other_#{s}").to_i) if has_attribute?(s) - end - end - end # == Schema Information @@ -1054,7 +973,6 @@ end # admin :boolean default(FALSE), not null # last_emailed_at :datetime # trust_level :integer not null -# email_private_messages :boolean default(TRUE) # approved :boolean default(FALSE), not null # approved_by_id :integer # approved_at :datetime @@ -1062,11 +980,9 @@ end # suspended_at :datetime # suspended_till :datetime # date_of_birth :date -# auto_track_topics_after_msecs :integer # views :integer default(0), not null # flag_level :integer default(0), not null # ip_address :inet -# new_topic_duration_minutes :integer # moderator :boolean default(FALSE) # blocked :boolean default(FALSE) # title :string(255) @@ -1074,7 +990,6 @@ end # primary_group_id :integer # locale :string(10) # registration_ip_address :inet -# last_redirected_to_top_at :datetime # trust_level_locked :boolean default(FALSE), not null # staged :boolean default(FALSE), not null # diff --git a/app/models/user_option.rb b/app/models/user_option.rb index 3dac2ded9e..46d7e67307 100644 --- a/app/models/user_option.rb +++ b/app/models/user_option.rb @@ -3,6 +3,8 @@ class UserOption < ActiveRecord::Base belongs_to :user before_create :set_defaults + after_save :update_tracked_topics + def set_defaults self.email_always = SiteSetting.default_email_always self.mailing_list_mode = SiteSetting.default_email_mailing_list_mode @@ -16,6 +18,9 @@ class UserOption < ActiveRecord::Base self.disable_jump_reply = SiteSetting.default_other_disable_jump_reply self.edit_history_public = SiteSetting.default_other_edit_history_public + self.new_topic_duration_minutes = SiteSetting.default_other_new_topic_duration_minutes + self.auto_track_topics_after_msecs = SiteSetting.default_other_auto_track_topics_after_msecs + if SiteSetting.default_email_digest_frequency.to_i <= 0 self.email_digests = false @@ -26,4 +31,70 @@ class UserOption < ActiveRecord::Base true end + + def update_tracked_topics + return unless auto_track_topics_after_msecs_changed? + TrackedTopicsUpdater.new(id, auto_track_topics_after_msecs).call + end + + def redirected_to_top_yet? + last_redirected_to_top_at.present? + end + + def update_last_redirected_to_top! + key = "user:#{id}:update_last_redirected_to_top" + delay = SiteSetting.active_user_rate_limit_secs + + # only update last_redirected_to_top_at once every minute + return unless $redis.setnx(key, "1") + $redis.expire(key, delay) + + # delay the update + Jobs.enqueue_in(delay / 2, :update_top_redirection, user_id: self.id, redirected_at: Time.zone.now) + end + + def should_be_redirected_to_top + redirected_to_top.present? + end + + def redirected_to_top + # redirect is enabled + return unless SiteSetting.redirect_users_to_top_page + # top must be in the top_menu + return unless SiteSetting.top_menu =~ /(^|\|)top(\||$)/i + # not enough topics + return unless period = SiteSetting.min_redirected_to_top_period + + if !user.seen_before? || (user.trust_level == 0 && !redirected_to_top_yet?) + update_last_redirected_to_top! + return { + reason: I18n.t('redirected_to_top_reasons.new_user'), + period: period + } + elsif user.last_seen_at < 1.month.ago + update_last_redirected_to_top! + return { + reason: I18n.t('redirected_to_top_reasons.not_seen_in_a_month'), + period: period + } + end + + # don't redirect to top + nil + end + + def treat_as_new_topic_start_date + duration = new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes.to_i + times = [case duration + when User::NewTopicDuration::ALWAYS + user.created_at + when User::NewTopicDuration::LAST_VISIT + user.previous_visit_at || user.user_stat.new_since + else + duration.minutes.ago + end, user.user_stat.new_since, Time.at(SiteSetting.min_new_topics_time).to_datetime] + + times.max + end + end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 5cf912d473..2c916e4756 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -70,6 +70,14 @@ class CurrentUserSerializer < BasicUserSerializer object.user_option.automatically_unpin_topics end + def should_be_redirected_to_top + object.user_option.should_be_redirected_to_top + end + + def redirected_to_top + object.user_option.redirected_to_top + end + def site_flagged_posts_count PostAction.flagged_posts_count end @@ -103,7 +111,7 @@ class CurrentUserSerializer < BasicUserSerializer end def include_redirected_to_top? - object.redirected_to_top.present? + object.user_option.redirected_to_top.present? end def custom_fields diff --git a/app/serializers/listable_topic_serializer.rb b/app/serializers/listable_topic_serializer.rb index 609bf1d72a..3589d7a231 100644 --- a/app/serializers/listable_topic_serializer.rb +++ b/app/serializers/listable_topic_serializer.rb @@ -45,7 +45,7 @@ class ListableTopicSerializer < BasicTopicSerializer def seen return true if !scope || !scope.user return true if object.user_data && !object.user_data.last_read_post_number.nil? - return true if object.created_at < scope.user.treat_as_new_topic_start_date + return true if object.created_at < scope.user.user_option.treat_as_new_topic_start_date false end diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb index 77c9d68540..440587a4d4 100644 --- a/app/serializers/user_option_serializer.rb +++ b/app/serializers/user_option_serializer.rb @@ -11,10 +11,21 @@ class UserOptionSerializer < ApplicationSerializer :disable_jump_reply, :digest_after_days, :automatically_unpin_topics, - :edit_history_public + :edit_history_public, + :auto_track_topics_after_msecs, + :new_topic_duration_minutes def include_edit_history_public? !SiteSetting.edit_history_visible_to_public end + + def auto_track_topics_after_msecs + object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs + end + + def new_topic_duration_minutes + object.new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes + end + end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 1ad96ce240..730a23da74 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -82,8 +82,6 @@ class UserSerializer < BasicUserSerializer :can_delete_all_posts private_attributes :locale, - :auto_track_topics_after_msecs, - :new_topic_duration_minutes, :muted_category_ids, :tracked_category_ids, :watched_category_ids, @@ -253,14 +251,6 @@ class UserSerializer < BasicUserSerializer ### PRIVATE ATTRIBUTES ### - def auto_track_topics_after_msecs - object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs - end - - def new_topic_duration_minutes - object.new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes - end - def muted_category_ids CategoryUser.lookup(object, :muted).pluck(:category_id) end diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index e028a0d14b..b3b8e9031c 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -18,7 +18,9 @@ class UserUpdater :disable_jump_reply, :edit_history_public, :automatically_unpin_topics, - :digest_after_days + :digest_after_days, + :new_topic_duration_minutes, + :auto_track_topics_after_msecs ] def initialize(actor, user) @@ -38,14 +40,6 @@ class UserUpdater user.name = attributes.fetch(:name) { user.name } user.locale = attributes.fetch(:locale) { user.locale } - if attributes[:auto_track_topics_after_msecs] - user.auto_track_topics_after_msecs = attributes[:auto_track_topics_after_msecs].to_i - end - - if attributes[:new_topic_duration_minutes] - user.new_topic_duration_minutes = attributes[:new_topic_duration_minutes].to_i - end - if guardian.can_grant_title?(user) user.title = attributes.fetch(:title) { user.title } end diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index ccdcaceace..d65c8f2ef7 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -32,9 +32,9 @@ duration = Rails.env.production? ? 60 : 0 if User.exec_sql("SELECT 1 FROM schema_migration_details WHERE EXISTS( SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'users' AND column_name = 'enable_quoting' + WHERE table_name = 'users' AND column_name = 'last_redirected_to_top_at' ) AND - name = 'AllowDefaultsOnUsersTable' AND + name = 'MoveTrackingOptionsToUserOptions' AND created_at < (current_timestamp at time zone 'UTC' - interval '#{duration} minutes') ").to_a.length > 0 @@ -54,7 +54,10 @@ if User.exec_sql("SELECT 1 FROM schema_migration_details edit_history_public automatically_unpin_topics digest_after_days - ].each do |column| + auto_track_topics_after_msecs + new_topic_duration_minutes + last_redirected_to_top_at +].each do |column| User.exec_sql("ALTER TABLE users DROP column IF EXISTS #{column}") end diff --git a/db/migrate/20160225050319_move_tracking_options_to_user_options.rb b/db/migrate/20160225050319_move_tracking_options_to_user_options.rb new file mode 100644 index 0000000000..87dabf3995 --- /dev/null +++ b/db/migrate/20160225050319_move_tracking_options_to_user_options.rb @@ -0,0 +1,16 @@ +class MoveTrackingOptionsToUserOptions < ActiveRecord::Migration + def change + add_column :user_options, :auto_track_topics_after_msecs, :integer + add_column :user_options, :new_topic_duration_minutes, :integer + add_column :user_options, :last_redirected_to_top_at, :datetime + + execute < true)), @user.treat_as_new_topic_start_date) + result = TopicQuery.new_filter(default_results(options.reverse_merge(:unordered => true)), @user.user_option.treat_as_new_topic_start_date) result = remove_muted_topics(result, @user) result = remove_muted_categories(result, @user, exclude: options[:category]) diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index 44f219d470..fad4faaea0 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -294,8 +294,8 @@ describe TopicQuery do context 'user with auto_track_topics list_unread' do before do - user.auto_track_topics_after_msecs = 0 - user.save + user.user_option.auto_track_topics_after_msecs = 0 + user.user_option.save end it 'only contains the partially read topic' do @@ -360,8 +360,8 @@ describe TopicQuery do expect(topic_query.list_new.topics).to eq([new_topic]) - user.new_topic_duration_minutes = 5 - user.save + user.user_option.new_topic_duration_minutes = 5 + user.user_option.save new_topic.created_at = 10.minutes.ago new_topic.save expect(topic_query.list_new.topics).to eq([]) @@ -561,8 +561,8 @@ describe TopicQuery do let!(:fully_read_archived) { Fabricate(:post, user: creator).topic } before do - user.auto_track_topics_after_msecs = 0 - user.save + user.user_option.auto_track_topics_after_msecs = 0 + user.user_option.save TopicUser.update_last_read(user, partially_read.id, 0, 0) TopicUser.update_last_read(user, fully_read.id, 1, 0) TopicUser.update_last_read(user, fully_read_closed.id, 1, 0) diff --git a/spec/fabricators/user_option_fabricator.rb b/spec/fabricators/user_option_fabricator.rb new file mode 100644 index 0000000000..f42ddaec9c --- /dev/null +++ b/spec/fabricators/user_option_fabricator.rb @@ -0,0 +1,2 @@ +Fabricator(:user_option) do +end diff --git a/spec/models/topic_user_spec.rb b/spec/models/topic_user_spec.rb index 9f9f5651a8..f51b90b335 100644 --- a/spec/models/topic_user_spec.rb +++ b/spec/models/topic_user_spec.rb @@ -48,7 +48,12 @@ describe TopicUser do let(:topic_creator_user) { TopicUser.get(topic, topic.user) } let(:post) { Fabricate(:post, topic: topic, user: user) } - let(:new_user) { Fabricate(:user, auto_track_topics_after_msecs: 1000) } + let(:new_user) { + u = Fabricate(:user) + u.user_option.update_columns(auto_track_topics_after_msecs: 1000) + u + } + let(:topic_new_user) { TopicUser.get(topic, new_user)} let(:yesterday) { DateTime.now.yesterday } @@ -68,15 +73,15 @@ describe TopicUser do describe 'notifications' do it 'should be set to tracking if auto_track_topics is enabled' do - user.update_column(:auto_track_topics_after_msecs, 0) + user.user_option.update_column(:auto_track_topics_after_msecs, 0) ensure_topic_user expect(TopicUser.get(topic, user).notification_level).to eq(TopicUser.notification_levels[:tracking]) end it 'should reset regular topics to tracking topics if auto track is changed' do ensure_topic_user - user.auto_track_topics_after_msecs = 0 - user.save + user.user_option.auto_track_topics_after_msecs = 0 + user.user_option.save expect(topic_user.notification_level).to eq(TopicUser.notification_levels[:tracking]) end diff --git a/spec/models/user_option_spec.rb b/spec/models/user_option_spec.rb new file mode 100644 index 0000000000..e3144989ad --- /dev/null +++ b/spec/models/user_option_spec.rb @@ -0,0 +1,92 @@ +require 'rails_helper' +require_dependency 'user_option' + +describe UserOption do + + describe "should_be_redirected_to_top" do + let!(:user) { Fabricate(:user) } + + it "should be redirected to top when there is a reason to" do + user.user_option.expects(:redirected_to_top).returns({ reason: "42" }) + expect(user.user_option.should_be_redirected_to_top).to eq(true) + end + + it "should not be redirected to top when there is no reason to" do + user.user_option.expects(:redirected_to_top).returns(nil) + expect(user.user_option.should_be_redirected_to_top).to eq(false) + end + + end + + describe ".redirected_to_top" do + let!(:user) { Fabricate(:user) } + + it "should have no reason when `SiteSetting.redirect_users_to_top_page` is disabled" do + SiteSetting.expects(:redirect_users_to_top_page).returns(false) + expect(user.user_option.redirected_to_top).to eq(nil) + end + + context "when `SiteSetting.redirect_users_to_top_page` is enabled" do + before { SiteSetting.expects(:redirect_users_to_top_page).returns(true) } + + it "should have no reason when top is not in the `SiteSetting.top_menu`" do + SiteSetting.expects(:top_menu).returns("latest") + expect(user.user_option.redirected_to_top).to eq(nil) + end + + context "and when top is in the `SiteSetting.top_menu`" do + before { SiteSetting.expects(:top_menu).returns("latest|top") } + + it "should have no reason when there are not enough topics" do + SiteSetting.expects(:min_redirected_to_top_period).returns(nil) + expect(user.user_option.redirected_to_top).to eq(nil) + end + + context "and there are enough topics" do + + before { SiteSetting.expects(:min_redirected_to_top_period).returns(:monthly) } + + describe "a new user" do + before do + user.stubs(:trust_level).returns(0) + user.stubs(:last_seen_at).returns(5.minutes.ago) + end + + it "should have a reason for the first visit" do + expect(user.user_option.redirected_to_top).to eq({ + reason: I18n.t('redirected_to_top_reasons.new_user'), + period: :monthly + }) + end + + it "should not have a reason for next visits" do + user.user_option.expects(:last_redirected_to_top_at).returns(10.minutes.ago) + user.user_option.expects(:update_last_redirected_to_top!).never + + expect(user.user_option.redirected_to_top).to eq(nil) + end + end + + describe "an older user" do + before { user.stubs(:trust_level).returns(1) } + + it "should have a reason when the user hasn't been seen in a month" do + user.last_seen_at = 2.months.ago + user.user_option.expects(:update_last_redirected_to_top!).once + + expect(user.user_option.redirected_to_top).to eq({ + reason: I18n.t('redirected_to_top_reasons.not_seen_in_a_month'), + period: :monthly + }) + end + + end + + end + + end + + end + + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5acf8bfb6d..dfbabbe039 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -986,95 +986,6 @@ describe User do end end - describe "should_be_redirected_to_top" do - let!(:user) { Fabricate(:user) } - - it "should be redirected to top when there is a reason to" do - user.expects(:redirected_to_top).returns({ reason: "42" }) - expect(user.should_be_redirected_to_top).to eq(true) - end - - it "should not be redirected to top when there is no reason to" do - user.expects(:redirected_to_top).returns(nil) - expect(user.should_be_redirected_to_top).to eq(false) - end - - end - - describe ".redirected_to_top" do - let!(:user) { Fabricate(:user) } - - it "should have no reason when `SiteSetting.redirect_users_to_top_page` is disabled" do - SiteSetting.expects(:redirect_users_to_top_page).returns(false) - expect(user.redirected_to_top).to eq(nil) - end - - context "when `SiteSetting.redirect_users_to_top_page` is enabled" do - before { SiteSetting.expects(:redirect_users_to_top_page).returns(true) } - - it "should have no reason when top is not in the `SiteSetting.top_menu`" do - SiteSetting.expects(:top_menu).returns("latest") - expect(user.redirected_to_top).to eq(nil) - end - - context "and when top is in the `SiteSetting.top_menu`" do - before { SiteSetting.expects(:top_menu).returns("latest|top") } - - it "should have no reason when there are not enough topics" do - SiteSetting.expects(:min_redirected_to_top_period).returns(nil) - expect(user.redirected_to_top).to eq(nil) - end - - context "and there are enough topics" do - - before { SiteSetting.expects(:min_redirected_to_top_period).returns(:monthly) } - - describe "a new user" do - before do - user.stubs(:trust_level).returns(0) - user.stubs(:last_seen_at).returns(5.minutes.ago) - end - - it "should have a reason for the first visit" do - user.expects(:last_redirected_to_top_at).returns(nil) - user.expects(:update_last_redirected_to_top!).once - - expect(user.redirected_to_top).to eq({ - reason: I18n.t('redirected_to_top_reasons.new_user'), - period: :monthly - }) - end - - it "should not have a reason for next visits" do - user.expects(:last_redirected_to_top_at).returns(10.minutes.ago) - user.expects(:update_last_redirected_to_top!).never - - expect(user.redirected_to_top).to eq(nil) - end - end - - describe "an older user" do - before { user.stubs(:trust_level).returns(1) } - - it "should have a reason when the user hasn't been seen in a month" do - user.last_seen_at = 2.months.ago - user.expects(:update_last_redirected_to_top!).once - - expect(user.redirected_to_top).to eq({ - reason: I18n.t('redirected_to_top_reasons.not_seen_in_a_month'), - period: :monthly - }) - end - - end - - end - - end - - end - - end describe "automatic avatar creation" do it "sets a system avatar for new users" do @@ -1281,9 +1192,8 @@ describe User do expect(options.edit_history_public).to eq(true) expect(options.automatically_unpin_topics).to eq(false) expect(options.email_direct).to eq(false) - - expect(user.new_topic_duration_minutes).to eq(-1) - expect(user.auto_track_topics_after_msecs).to eq(0) + expect(options.new_topic_duration_minutes).to eq(-1) + expect(options.auto_track_topics_after_msecs).to eq(0) expect(CategoryUser.lookup(user, :watching).pluck(:category_id)).to eq([1]) expect(CategoryUser.lookup(user, :tracking).pluck(:category_id)).to eq([2]) diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb index 3a9db1a231..16320fdfb8 100644 --- a/spec/serializers/user_serializer_spec.rb +++ b/spec/serializers/user_serializer_spec.rb @@ -19,6 +19,8 @@ describe UserSerializer do it "serializes options correctly" do # so we serialize more stuff SiteSetting.edit_history_visible_to_public = false + SiteSetting.default_other_auto_track_topics_after_msecs = 0 + SiteSetting.default_other_new_topic_duration_minutes = 60*24 user = Fabricate.build(:user, user_profile: Fabricate.build(:user_profile), @@ -29,6 +31,8 @@ describe UserSerializer do json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json expect(json[:user_option][:edit_history_public]).to eq(true) + expect(json[:user_option][:new_topic_duration_minutes]).to eq(60*24) + expect(json[:user_option][:auto_track_topics_after_msecs]).to eq(0) end end diff --git a/spec/services/user_updater_spec.rb b/spec/services/user_updater_spec.rb index f94d2bb9e7..7866206bfe 100644 --- a/spec/services/user_updater_spec.rb +++ b/spec/services/user_updater_spec.rb @@ -46,13 +46,18 @@ describe UserUpdater do updater.update(bio_raw: 'my new bio', email_always: 'true', mailing_list_mode: true, - digest_after_days: "8") + digest_after_days: "8", + new_topic_duration_minutes: 100, + auto_track_topics_after_msecs: 101 + ) user.reload expect(user.user_profile.bio_raw).to eq 'my new bio' expect(user.user_option.email_always).to eq true expect(user.user_option.mailing_list_mode).to eq true expect(user.user_option.digest_after_days).to eq 8 + expect(user.user_option.new_topic_duration_minutes).to eq 100 + expect(user.user_option.auto_track_topics_after_msecs).to eq 101 end context 'when update succeeds' do From 3b9223c5da0812711d36921277b05265a0b370ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 18 Feb 2016 16:56:45 +0100 Subject: [PATCH 104/140] bump email_reply_trimmer to latest version --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 12fa31a0af..3f5a46fb76 100644 --- a/Gemfile +++ b/Gemfile @@ -64,7 +64,7 @@ gem 'aws-sdk', require: false gem 'excon', require: false gem 'unf', require: false -gem 'email_reply_trimmer', '0.0.6' +gem 'email_reply_trimmer', '0.0.8' # note: for image_optim to correctly work you need to follow # https://github.com/toy/image_optim diff --git a/Gemfile.lock b/Gemfile.lock index 55d8c3d124..a614c28a89 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,7 +76,7 @@ GEM docile (1.1.5) domain_name (0.5.25) unf (>= 0.0.5, < 1.0.0) - email_reply_trimmer (0.0.6) + email_reply_trimmer (0.0.8) ember-data-source (1.0.0.beta.16.1) ember-source (~> 1.8) ember-handlebars-template (0.1.5) @@ -411,7 +411,7 @@ DEPENDENCIES byebug certified discourse-qunit-rails - email_reply_trimmer (= 0.0.6) + email_reply_trimmer (= 0.0.8) ember-rails ember-source (= 1.12.2) excon From 8a1a9f60a2df30661b842142200b2449bd2801ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 18 Feb 2016 18:42:10 +0100 Subject: [PATCH 105/140] FIX: double click counters --- app/assets/javascripts/discourse/views/post.js.es6 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index 21535c72d5..8dd08a5b61 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -186,10 +186,12 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { const $link = $(this), href = $link.attr('href'); - let valid = !lc.internal && href === lc.url; + let valid = href === lc.url; // this might be an attachment - if (lc.internal) { valid = href.indexOf(lc.url) >= 0; } + if (lc.internal && /^\/uploads\//.test(lc.url)) { + valid = href.indexOf(lc.url) >= 0; + } if (valid) { // don't display badge counts on category badge & oneboxes (unless when explicitely stated) From 283ff4c7f8503f0974f2447a40a97e6f5be48e84 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 18 Feb 2016 14:03:00 -0500 Subject: [PATCH 106/140] move code for bulk adding users to a group from controller to model --- app/controllers/admin/groups_controller.rb | 23 +------------------- app/models/group.rb | 25 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 62653c427e..920cc34955 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -28,28 +28,7 @@ class Admin::GroupsController < Admin::AdminController if group.present? users = (params[:users] || []).map {|u| u.downcase} user_ids = User.where("username_lower in (:users) OR email IN (:users)", users: users).pluck(:id) - - if user_ids.present? - Group.exec_sql("INSERT INTO group_users - (group_id, user_id, created_at, updated_at) - SELECT #{group.id}, - u.id, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - FROM users AS u - WHERE u.id IN (#{user_ids.join(', ')}) - AND NOT EXISTS(SELECT 1 FROM group_users AS gu - WHERE gu.user_id = u.id AND - gu.group_id = #{group.id})") - - if group.primary_group? - User.where(id: user_ids).update_all(primary_group_id: group.id) - end - - if group.title.present? - User.where(id: user_ids).update_all(title: group.title) - end - end + group.bulk_add(user_ids) if user_ids.present? end render json: success_json diff --git a/app/models/group.rb b/app/models/group.rb index a508335a77..148b1e9e8d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -335,6 +335,31 @@ class Group < ActiveRecord::Base self.find_by(incoming_email: Email.downcase(email)) end + def bulk_add(user_ids) + if user_ids.present? + Group.exec_sql("INSERT INTO group_users + (group_id, user_id, created_at, updated_at) + SELECT #{self.id}, + u.id, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + FROM users AS u + WHERE u.id IN (#{user_ids.join(', ')}) + AND NOT EXISTS(SELECT 1 FROM group_users AS gu + WHERE gu.user_id = u.id AND + gu.group_id = #{self.id})") + + if self.primary_group? + User.where(id: user_ids).update_all(primary_group_id: self.id) + end + + if self.title.present? + User.where(id: user_ids).update_all(title: self.title) + end + end + true + end + protected def name_format_validator From e204144a582ec8c3a4424516b3b1b00d2b27655f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 18 Feb 2016 23:19:14 +0100 Subject: [PATCH 107/140] UsernameValidator error messages weren't matching the code --- app/models/username_validator.rb | 2 +- config/locales/server.en.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/username_validator.rb b/app/models/username_validator.rb index 1ef41e17ef..0dbae4bae6 100644 --- a/app/models/username_validator.rb +++ b/app/models/username_validator.rb @@ -71,7 +71,7 @@ class UsernameValidator def username_first_char_valid? return unless errors.empty? if username[0] =~ /\W/ - self.errors << I18n.t(:'user.username.must_begin_with_alphanumeric') + self.errors << I18n.t(:'user.username.must_begin_with_alphanumeric_or_underscore') end end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 281d536546..4fe2c9cc74 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1397,8 +1397,8 @@ en: characters: "must only include numbers, letters and underscores" unique: "must be unique" blank: "must be present" - must_begin_with_alphanumeric: "must begin with a letter or number or an underscore" - must_end_with_alphanumeric: "must end with a letter or number or an underscore" + must_begin_with_alphanumeric_or_underscore: "must begin with a letter, a number or an underscore" + must_end_with_alphanumeric: "must end with a letter or a number" must_not_contain_two_special_chars_in_seq: "must not contain a sequence of 2 or more special chars (.-_)" must_not_end_with_confusing_suffix: "must not end with a confusing suffix like .json or .png etc." email: From 1a0a3645039abd78aec7f194785eb4b2a2d2ae28 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 19 Feb 2016 10:04:37 +0800 Subject: [PATCH 108/140] SECURITY: Upgrade Sprockets. * Advisory: CVE-2014-7819 * URL: https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY --- Gemfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a614c28a89..b78e7471a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -328,9 +328,9 @@ GEM sass (3.2.19) sass-rails (4.0.5) railties (>= 4.0.0, < 5.0) - sass (~> 3.2.0) - sprockets (~> 2.8, <= 2.11.0) - sprockets-rails (~> 2.0.0) + sass (~> 3.2.2) + sprockets (~> 2.8, < 3.0) + sprockets-rails (~> 2.0) seed-fu (2.3.5) activerecord (>= 3.1, < 4.3) activesupport (>= 3.1, < 4.3) @@ -362,15 +362,15 @@ GEM spork-rails (4.0.0) rails (>= 3.0.0, < 5) spork (>= 1.0rc0) - sprockets (2.11.0) + sprockets (2.12.4) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.1) + sprockets-rails (2.3.3) actionpack (>= 3.0) activesupport (>= 3.0) - sprockets (~> 2.8) + sprockets (>= 2.8, < 4.0) stackprof (0.2.7) therubyracer (0.12.2) libv8 (~> 3.16.14.0) From 3de390c06794568565982e0fa9988f54d7037573 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 19 Feb 2016 13:26:13 +1100 Subject: [PATCH 109/140] quote fields in case they are still in the db --- app/jobs/scheduled/enqueue_digest_emails.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/scheduled/enqueue_digest_emails.rb b/app/jobs/scheduled/enqueue_digest_emails.rb index ea74f98ed2..9c10f69573 100644 --- a/app/jobs/scheduled/enqueue_digest_emails.rb +++ b/app/jobs/scheduled/enqueue_digest_emails.rb @@ -19,8 +19,8 @@ module Jobs .joins(:user_option) .not_suspended .where(user_options: {email_digests: true}) - .where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)") - .where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)") + .where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * user_options.digest_after_days)") + .where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * user_options.digest_after_days)") .where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * #{SiteSetting.delete_digest_email_after_days})") # If the site requires approval, make sure the user is approved From ab06f86fbe2a0bba670040d5c905ef8df0d87f9f Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 19 Feb 2016 13:56:52 +1100 Subject: [PATCH 110/140] FEATURE: allow users to control how many previous replies they get - always means we always send previous replies with every email - never means we do not - "unless previously sent" ... is the default, in which we only email you each reply once The default_email_previous_replies site setting can control this toggle --- .../discourse/controllers/preferences.js.es6 | 6 ++++ .../javascripts/discourse/models/user.js.es6 | 1 + .../discourse/templates/user/preferences.hbs | 7 ++++ app/mailers/user_notifications.rb | 7 +++- app/models/previous_replies_site_setting.rb | 22 +++++++++++++ app/models/user_option.rb | 5 +++ app/serializers/user_option_serializer.rb | 3 +- app/services/user_updater.rb | 3 +- config/locales/client.en.yml | 5 +++ config/locales/server.en.yml | 1 + config/site_settings.yml | 3 ++ ..._email_previous_replies_to_user_options.rb | 5 +++ spec/mailers/user_notifications_spec.rb | 32 +++++++++++++++---- 13 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 app/models/previous_replies_site_setting.rb create mode 100644 db/migrate/20160225050320_add_email_previous_replies_to_user_options.rb diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index 1efdaff99b..5231e4fa43 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -62,6 +62,12 @@ export default Ember.Controller.extend(CanCheckEmails, { return this.siteSettings.available_locales.split('|').map(s => ({ name: s, value: s })); }, + previousRepliesOptions: [ + {name: I18n.t('user.email_previous_replies.always'), value: 0}, + {name: I18n.t('user.email_previous_replies.unless_emailed'), value: 1}, + {name: I18n.t('user.email_previous_replies.never'), value: 2} + ], + digestFrequencies: [{ name: I18n.t('user.email_digests.daily'), value: 1 }, { name: I18n.t('user.email_digests.every_three_days'), value: 3 }, { name: I18n.t('user.email_digests.weekly'), value: 7 }, diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index c57574431e..9eeb9f87b3 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -159,6 +159,7 @@ const User = RestModel.extend({ 'email_digests', 'email_direct', 'email_private_messages', + 'email_previous_replies', 'dynamic_favicon', 'enable_quoting', 'disable_jump_reply', diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index 06bab9cbf4..6e8c026a4d 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -176,6 +176,10 @@
    {{/if}} {{/if}} +
    + + {{combo-box valueAttribute="value" content=previousRepliesOptions value=model.user_option.email_previous_replies}} +
    {{preference-checkbox labelKey="user.email_private_messages" checked=model.user_option.email_private_messages}} {{preference-checkbox labelKey="user.email_direct" checked=model.user_option.email_direct}} {{preference-checkbox labelKey="user.mailing_list_mode" checked=model.user_option.mailing_list_mode}} @@ -188,6 +192,9 @@ {{i18n 'user.email.frequency_immediately'}} {{/if}} + + +
    diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 52db55f8ab..e8def9ecce 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -193,6 +193,11 @@ class UserNotifications < ActionMailer::Base end def self.get_context_posts(post, topic_user) + user_option = topic_user.try(:user).try(:user_option) + if user_option && (user_option.email_previous_replies == UserOption.previous_replies_type[:never]) + return [] + end + allowed_post_types = [Post.types[:regular]] allowed_post_types << Post.types[:whisper] if topic_user.try(:user).try(:staff?) @@ -204,7 +209,7 @@ class UserNotifications < ActionMailer::Base .order('created_at desc') .limit(SiteSetting.email_posts_context) - if topic_user && topic_user.last_emailed_post_number + if topic_user && topic_user.last_emailed_post_number && user_option.try(:email_previous_replies) == UserOption.previous_replies_type[:unless_emailed] context_posts = context_posts.where("post_number > ?", topic_user.last_emailed_post_number) end diff --git a/app/models/previous_replies_site_setting.rb b/app/models/previous_replies_site_setting.rb new file mode 100644 index 0000000000..eac55ae523 --- /dev/null +++ b/app/models/previous_replies_site_setting.rb @@ -0,0 +1,22 @@ +require_dependency 'enum_site_setting' + +class PreviousRepliesSiteSetting < EnumSiteSetting + + def self.valid_value?(val) + val.to_i.to_s == val.to_s && + values.any? { |v| v[:value] == val.to_i } + end + + def self.values + @values ||= [ + { name: 'user.email_previous_replies.always', value: 0 }, + { name: 'user.email_previous_replies.unless_emailed', value: 1 }, + { name: 'user.email_previous_replies.never', value: 2 }, + ] + end + + def self.translate_names? + true + end + +end diff --git a/app/models/user_option.rb b/app/models/user_option.rb index 46d7e67307..e6384f061b 100644 --- a/app/models/user_option.rb +++ b/app/models/user_option.rb @@ -5,12 +5,17 @@ class UserOption < ActiveRecord::Base after_save :update_tracked_topics + def self.previous_replies_type + @previous_replies_type ||= Enum.new(always: 0, unless_emailed: 1, never: 2) + end + def set_defaults self.email_always = SiteSetting.default_email_always self.mailing_list_mode = SiteSetting.default_email_mailing_list_mode self.email_direct = SiteSetting.default_email_direct self.automatically_unpin_topics = SiteSetting.default_topics_automatic_unpin self.email_private_messages = SiteSetting.default_email_private_messages + self.email_previous_replies = SiteSetting.default_email_previous_replies self.enable_quoting = SiteSetting.default_other_enable_quoting self.external_links_in_new_tab = SiteSetting.default_other_external_links_in_new_tab diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb index 440587a4d4..bf43b41a36 100644 --- a/app/serializers/user_option_serializer.rb +++ b/app/serializers/user_option_serializer.rb @@ -13,7 +13,8 @@ class UserOptionSerializer < ApplicationSerializer :automatically_unpin_topics, :edit_history_public, :auto_track_topics_after_msecs, - :new_topic_duration_minutes + :new_topic_duration_minutes, + :email_previous_replies def include_edit_history_public? diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index b3b8e9031c..3b24efff08 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -20,7 +20,8 @@ class UserUpdater :automatically_unpin_topics, :digest_after_days, :new_topic_duration_minutes, - :auto_track_topics_after_msecs + :auto_track_topics_after_msecs, + :email_previous_replies ] def initialize(actor, user) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index cdb5b7bc88..c6bdcc24b1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -627,6 +627,11 @@ en: website: "Web Site" email_settings: "Email" + email_previous_replies: + title: "Include previous replies" + unless_emailed: "unless previously sent" + always: "always" + never: "never" email_digests: title: "When I don't visit here, send an email digest of what's new:" daily: "daily" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4fe2c9cc74..ad4fbd5c7d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1243,6 +1243,7 @@ en: default_email_direct: "Send an email when someone quotes/replies to/mentions or invites the user by default." default_email_mailing_list_mode: "Send an email for every new post by default." default_email_always: "Send an email notification even when the user is active by default." + default_email_previous_replies: "Include previous replies in emails by default." default_other_new_topic_duration_minutes: "Global default condition for which a topic is considered new." default_other_auto_track_topics_after_msecs: "Global default time before a topic is automatically tracked." diff --git a/config/site_settings.yml b/config/site_settings.yml index 6937a8de35..dbf80b3a3e 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1064,6 +1064,9 @@ user_preferences: default_email_direct: true default_email_mailing_list_mode: false default_email_always: false + default_email_previous_replies: + enum: 'PreviousRepliesSiteSetting' + default: 1 default_other_new_topic_duration_minutes: enum: 'NewTopicDurationSiteSetting' diff --git a/db/migrate/20160225050320_add_email_previous_replies_to_user_options.rb b/db/migrate/20160225050320_add_email_previous_replies_to_user_options.rb new file mode 100644 index 0000000000..5f9aec18a0 --- /dev/null +++ b/db/migrate/20160225050320_add_email_previous_replies_to_user_options.rb @@ -0,0 +1,5 @@ +class AddEmailPreviousRepliesToUserOptions < ActiveRecord::Migration + def change + add_column :user_options, :email_previous_replies, :integer, null: false, default: 1 + end +end diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index 947dd9a466..a58c2d16f5 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -7,12 +7,12 @@ describe UserNotifications do describe "#get_context_posts" do it "does not include hidden/deleted/user_deleted posts in context" do post1 = create_post - post2 = Fabricate(:post, topic: post1.topic, deleted_at: 1.day.ago) - post3 = Fabricate(:post, topic: post1.topic, user_deleted: true) - post4 = Fabricate(:post, topic: post1.topic, hidden: true) - post5 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:moderator_action]) - post6 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:small_action]) - post7 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:whisper]) + _post2 = Fabricate(:post, topic: post1.topic, deleted_at: 1.day.ago) + _post3 = Fabricate(:post, topic: post1.topic, user_deleted: true) + _post4 = Fabricate(:post, topic: post1.topic, hidden: true) + _post5 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:moderator_action]) + _post6 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:small_action]) + _post7 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:whisper]) last = Fabricate(:post, topic: post1.topic) # default is only post #1 @@ -21,6 +21,26 @@ describe UserNotifications do tu = TopicUser.new(topic: post1.topic, user: build(:moderator)) expect(UserNotifications.get_context_posts(last, tu).count).to eq(2) end + + it "allows users to control context" do + post1 = create_post + _post2 = Fabricate(:post, topic: post1.topic) + post3 = Fabricate(:post, topic: post1.topic) + + user = Fabricate(:user) + TopicUser.change(user.id, post1.topic_id, last_emailed_post_number: 1) + topic_user = TopicUser.find_by(user_id: user.id, topic_id: post1.topic_id) + # to avoid reloads after update_columns + user = topic_user.user + expect(UserNotifications.get_context_posts(post3, topic_user).count).to eq(1) + + user.user_option.update_columns(email_previous_replies: UserOption.previous_replies_type[:never]) + expect(UserNotifications.get_context_posts(post3, topic_user).count).to eq(0) + + user.user_option.update_columns(email_previous_replies: UserOption.previous_replies_type[:always]) + expect(UserNotifications.get_context_posts(post3, topic_user).count).to eq(2) + + end end describe ".signup" do From c230c11707ddc685f49117988e20aab7fb070635 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 19 Feb 2016 17:03:23 +1100 Subject: [PATCH 111/140] FEATURE: stop removing empty categories users have access to from /categories page This is particularly important for heirarchies of categories where a parent is empty Also, if we hide the blank category, how are we going to create the first topic? Old behavior was hacky. --- app/models/category_list.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/models/category_list.rb b/app/models/category_list.rb index 6f445feaa1..642a18643d 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -145,15 +145,9 @@ class CategoryList end - # Remove any empty categories unless we can create them (so we can see the controls) def prune_empty - if !@guardian.can_create?(Category) - # Remove categories with no featured topics unless we have the ability to edit one - @categories.delete_if do |c| - c.displayable_topics.blank? && c.description.blank? - end - elsif !SiteSetting.allow_uncategorized_topics - # Don't show uncategorized to admins either, if uncategorized topics are not allowed + if @guardian.can_create?(Category) && !SiteSetting.allow_uncategorized_topics + # HACK: Don't show uncategorized to admins either, if uncategorized topics are not allowed # and there are none. @categories.delete_if do |c| c.uncategorized? && c.displayable_topics.blank? From 02002afd3f9cb871ca4526524cb62b6eb1b673b6 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 19 Feb 2016 17:08:08 +1100 Subject: [PATCH 112/140] clean up hack --- app/models/category_list.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/category_list.rb b/app/models/category_list.rb index 642a18643d..b6ffafb76a 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -146,9 +146,8 @@ class CategoryList def prune_empty - if @guardian.can_create?(Category) && !SiteSetting.allow_uncategorized_topics - # HACK: Don't show uncategorized to admins either, if uncategorized topics are not allowed - # and there are none. + unless SiteSetting.allow_uncategorized_topics + # HACK: Don't show uncategorized to anyone if not allowed @categories.delete_if do |c| c.uncategorized? && c.displayable_topics.blank? end From f18f6dc31f2366f28511de4276e797616ef16f58 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 19 Feb 2016 17:43:26 +1100 Subject: [PATCH 113/140] correct spec to stop checking for empty category suppression --- .../category_list_spec.rb | 49 ++----------------- 1 file changed, 5 insertions(+), 44 deletions(-) rename spec/{components => models}/category_list_spec.rb (74%) diff --git a/spec/components/category_list_spec.rb b/spec/models/category_list_spec.rb similarity index 74% rename from spec/components/category_list_spec.rb rename to spec/models/category_list_spec.rb index bab1d41ae1..bb4f3d2d83 100644 --- a/spec/components/category_list_spec.rb +++ b/spec/models/category_list_spec.rb @@ -16,8 +16,8 @@ describe CategoryList do # uncategorized + this expect(CategoryList.new(Guardian.new admin).categories.count).to eq(2) - expect(CategoryList.new(Guardian.new user).categories.count).to eq(0) - expect(CategoryList.new(Guardian.new nil).categories.count).to eq(0) + expect(CategoryList.new(Guardian.new user).categories.count).to eq(1) + expect(CategoryList.new(Guardian.new nil).categories.count).to eq(1) end it "doesn't show topics that you can't view" do @@ -51,55 +51,16 @@ describe CategoryList do let!(:topic_category) { Fabricate(:category) } - context "without a featured topic" do - - it "should not return empty categories" do - expect(category_list.categories).to be_blank - end - - it "returns empty categories for those who can create them" do - SiteSetting.stubs(:allow_uncategorized_topics).returns(true) - Guardian.any_instance.expects(:can_create?).with(Category).returns(true) - expect(category_list.categories).not_to be_blank - end - - it "returns empty categories with descriptions" do - Fabricate(:category, description: 'The category description.') - Guardian.any_instance.expects(:can_create?).with(Category).returns(false) - expect(category_list.categories).not_to be_blank - end - - it 'returns the empty category and a non-empty category for those who can create them' do - SiteSetting.stubs(:allow_uncategorized_topics).returns(true) - Fabricate(:topic, category: Fabricate(:category)) - Guardian.any_instance.expects(:can_create?).with(Category).returns(true) - expect(category_list.categories.size).to eq(3) - expect(category_list.categories).to include(topic_category) - end - - it "doesn't return empty uncategorized category to admins if allow_uncategorized_topics is false" do - SiteSetting.stubs(:allow_uncategorized_topics).returns(false) - expect(CategoryList.new(Guardian.new(user)).categories).to be_empty - expect(CategoryList.new(Guardian.new(admin)).categories.map(&:id)).not_to include(SiteSetting.uncategorized_category_id) - end - - end - context "with a topic in a category" do let!(:topic) { Fabricate(:topic, category: topic_category) } - let(:category) { category_list.categories.first } + let(:category) { category_list.categories.find{|c| c.id == topic_category.id} } it "should return the category" do expect(category).to be_present - end - - it "returns the correct category" do expect(category.id).to eq(topic_category.id) - end - - it "should contain our topic" do expect(category.featured_topics.include?(topic)).to eq(true) end + end context "with pinned topics in a category" do @@ -107,7 +68,7 @@ describe CategoryList do let!(:topic2) { Fabricate(:topic, category: topic_category, bumped_at: 5.minutes.ago) } let!(:topic3) { Fabricate(:topic, category: topic_category, bumped_at: 2.minutes.ago) } let!(:pinned) { Fabricate(:topic, category: topic_category, pinned_at: 10.minutes.ago, bumped_at: 10.minutes.ago) } - let(:category) { category_list.categories.first } + let(:category) { category_list.categories.find{|c| c.id == topic_category.id} } before do SiteSetting.stubs(:category_featured_topics).returns(2) From 7a261e5e4f84f2ff52b85edae77157797be2e4b5 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 19 Feb 2016 15:22:41 +0800 Subject: [PATCH 114/140] UX: Hide close mobile navigation on click. --- .../discourse/components/mobile-nav.js.es6 | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/components/mobile-nav.js.es6 b/app/assets/javascripts/discourse/components/mobile-nav.js.es6 index 939e3010fa..cee8a2111a 100644 --- a/app/assets/javascripts/discourse/components/mobile-nav.js.es6 +++ b/app/assets/javascripts/discourse/components/mobile-nav.js.es6 @@ -1,6 +1,9 @@ +import { on, observes } from 'ember-addons/ember-computed-decorators'; + export default Ember.Component.extend({ - _init: function(){ + @on('init') + _init() { if (!this.get('site.mobileView')) { var classes = this.get('desktopClass'); if (classes) { @@ -8,16 +11,17 @@ export default Ember.Component.extend({ this.set('classNames', classes); } } - }.on('init'), + }, tagName: 'ul', classNames: ['mobile-nav'], - currentPathChanged: function(){ + @observes('currentPath') + currentPathChanged() { this.set('expanded', false); Em.run.next(() => this._updateSelectedHtml()); - }.observes('currentPath'), + }, _updateSelectedHtml(){ const active = this.$('.active'); @@ -26,10 +30,22 @@ export default Ember.Component.extend({ } }, - didInsertElement(){ + didInsertElement() { this._updateSelectedHtml(); }, + @on('didInsertElement') + _bindClick() { + this.$().on("click.mobile-nav", 'ul li', () => { + this.set('expanded', false); + }); + }, + + @on('willDestroyElement') + _unbindClick() { + this.$().off("click.mobile-nav", 'ul li'); + }, + actions: { toggleExpanded(){ this.toggleProperty('expanded'); From 665a87a32ffa391f39905908521e7ec91bdbd24c Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 19 Feb 2016 18:28:17 +1100 Subject: [PATCH 115/140] UX: revert full page search focus on magnifying glass click --- .../javascripts/discourse/controllers/header.js.es6 | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/header.js.es6 b/app/assets/javascripts/discourse/controllers/header.js.es6 index 07addf2c1c..57401135df 100644 --- a/app/assets/javascripts/discourse/controllers/header.js.es6 +++ b/app/assets/javascripts/discourse/controllers/header.js.es6 @@ -21,13 +21,7 @@ const HeaderController = Ember.Controller.extend({ actions: { toggleSearch() { - // there may be a cleaner way, but this is so trivial code wise - const $fullpageSearch = $('input.full-page-search'); - if ($fullpageSearch.length === 1) { - $fullpageSearch.focus().select(); - } else { - this.toggleProperty('searchVisible'); - } + this.toggleProperty('searchVisible'); }, showUserMenu() { if (!this.get('userMenuVisible')) { From 5e329898f6b4849e3584bb6d6b4dbdd73420525e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 19 Feb 2016 15:32:10 +0800 Subject: [PATCH 116/140] UX: Don't display span if there is no count. --- app/assets/javascripts/admin/templates/groups_type.hbs | 6 +++++- app/assets/javascripts/discourse/models/group.js.es6 | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/templates/groups_type.hbs b/app/assets/javascripts/admin/templates/groups_type.hbs index 980ca08c8f..3c4fc3e506 100644 --- a/app/assets/javascripts/admin/templates/groups_type.hbs +++ b/app/assets/javascripts/admin/templates/groups_type.hbs @@ -4,7 +4,11 @@
      {{#each group in controller}}
    • - {{#link-to "adminGroup" group.type group.name}}{{group.name}} {{group.userCountDisplay}}{{/link-to}} + {{#link-to "adminGroup" group.type group.name}}{{group.name}} + {{#if group.userCountDisplay}} + {{group.userCountDisplay}} + {{/if}} + {{/link-to}}
    • {{/each}}
    diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index d0048cc957..a34c6dccae 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -17,11 +17,11 @@ const Group = Discourse.Model.extend({ return this.get("automatic") ? "automatic" : "custom"; }.property("automatic"), - userCountDisplay: function(){ - var c = this.get('user_count'); + @computed('user_count') + userCountDisplay(userCount) { // don't display zero its ugly - if (c > 0) { return c; } - }.property('user_count'), + if (userCount > 0) { return userCount; } + }, findMembers() { if (Em.isEmpty(this.get('name'))) { return ; } From afa4e58efdaa27e33196952ed6078d6075fac231 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 19 Feb 2016 15:52:47 +0800 Subject: [PATCH 117/140] Revert "SECURITY: Upgrade Sprockets." This reverts commit 1a0a3645039abd78aec7f194785eb4b2a2d2ae28. --- Gemfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b78e7471a5..a614c28a89 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -328,9 +328,9 @@ GEM sass (3.2.19) sass-rails (4.0.5) railties (>= 4.0.0, < 5.0) - sass (~> 3.2.2) - sprockets (~> 2.8, < 3.0) - sprockets-rails (~> 2.0) + sass (~> 3.2.0) + sprockets (~> 2.8, <= 2.11.0) + sprockets-rails (~> 2.0.0) seed-fu (2.3.5) activerecord (>= 3.1, < 4.3) activesupport (>= 3.1, < 4.3) @@ -362,15 +362,15 @@ GEM spork-rails (4.0.0) rails (>= 3.0.0, < 5) spork (>= 1.0rc0) - sprockets (2.12.4) + sprockets (2.11.0) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.3.3) + sprockets-rails (2.0.1) actionpack (>= 3.0) activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) + sprockets (~> 2.8) stackprof (0.2.7) therubyracer (0.12.2) libv8 (~> 3.16.14.0) From 97130463d6c6f92d0da9eb6de4b31f7e33e4be1d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 19 Feb 2016 12:19:20 -0500 Subject: [PATCH 118/140] FEATURE: show a new modal when suspended users try to log in --- app/assets/javascripts/discourse/controllers/login.js.es6 | 3 +++ app/controllers/session_controller.rb | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index b2eed5ceed..cd81f119c0 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -69,6 +69,9 @@ export default Ember.Controller.extend(ModalFunctionality, { sentTo: result.sent_to_email, currentEmail: result.current_email }); + } else if (result.reason === 'suspended' ) { + self.send("closeModal"); + bootbox.alert(result.error); } else { self.flash(result.error, 'error'); } diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index bcf9db0ee4..a34b31b704 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -261,8 +261,10 @@ class SessionController < ApplicationController def failed_to_login(user) message = user.suspend_reason ? "login.suspended_with_reason" : "login.suspended" - render json: { error: I18n.t(message, { date: I18n.l(user.suspended_till, format: :date_only), - reason: user.suspend_reason}) } + render json: { + error: I18n.t(message, { date: I18n.l(user.suspended_till, format: :date_only), reason: user.suspend_reason}), + reason: 'suspended' + } end def login(user) From 6c684944c57c9f7210d4a146229ec8ae771498f9 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 19 Feb 2016 09:19:36 -0800 Subject: [PATCH 119/140] add link to mail-tester.com in test email --- config/locales/server.en.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index ad4fbd5c7d..67f58b622f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1485,6 +1485,8 @@ en: - If you run your own mail server, check to make sure the IPs of your mail server are [not on any email blacklists][4]. Also verify that it is definitely sending a fully-qualified hostname that resolves in DNS in its HELO message. If not, this will cause your email to be rejected by many mail services. + - Send a test email to [mail-tester.com][mt] to verify that everything is working correctly. + (The *easy* way is to create a free account on [Mandrill][md] or [Mailgun][mg] or [Mailjet][mj], which have free generous free mailing plans and will be fine for small communities. You'll still need to set up the SPF and DKIM records in your DNS, though!) We hope you received this email deliverability test OK! @@ -1503,6 +1505,7 @@ en: [md]: http://mandrill.com [mg]: http://www.mailgun.com/ [mj]: https://www.mailjet.com/pricing + [mt]: http://www.mail-tester.com/ new_version_mailer: subject_template: "[%{site_name}] New Discourse version, update available" From e8d837269be58aa53e5275df7fd510778819c453 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 19 Feb 2016 15:21:05 -0500 Subject: [PATCH 120/140] FEATURE: pending flags reminder is sent as a group message to staff instead of sending an email to the contact email site setting. --- app/jobs/scheduled/pending_flags_reminder.rb | 10 ++++++++-- config/locales/server.en.yml | 19 ++++++++----------- spec/jobs/pending_flags_reminder_spec.rb | 8 ++++---- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/jobs/scheduled/pending_flags_reminder.rb b/app/jobs/scheduled/pending_flags_reminder.rb index 8b196d4ae6..72e75372be 100644 --- a/app/jobs/scheduled/pending_flags_reminder.rb +++ b/app/jobs/scheduled/pending_flags_reminder.rb @@ -11,8 +11,14 @@ module Jobs PostAction.flagged_posts_count > 0 && FlagQuery.flagged_post_actions('active').where('post_actions.created_at < ?', SiteSetting.notify_about_flags_after.to_i.hours.ago).pluck(:id).count > 0 - message = PendingFlagsMailer.notify - Email::Sender.new(message, :pending_flags_reminder).send + PostCreator.create( + Discourse.system_user, + target_group_names: ["staff"], + archetype: Archetype.private_message, + subtype: TopicSubtype.system_message, + title: I18n.t('flags_reminder.subject_template', { count: PostAction.flagged_posts_count }), + raw: I18n.t('flags_reminder.flags_were_submitted', { count: SiteSetting.notify_about_flags_after }) + ) end end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 67f58b622f..1524c92f1f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1409,6 +1409,14 @@ en: blocked: "New registrations are not allowed from your IP address." max_new_accounts_per_registration_ip: "New registrations are not allowed from your IP address (maximum limit reached). Contact a staff member." + flags_reminder: + flags_were_submitted: + one: "Flags were submitted over 1 hour ago. Please review them." + other: "Flags were submitted over %{count} hours ago. Please review them." + subject_template: + one: "1 flag waiting to be handled" + other: "%{count} flags waiting to be handled" + unsubscribe_mailer: subject_template: "Confirm you no longer want to receive email updates from %{site_title}" text_body_template: | @@ -1543,17 +1551,6 @@ en: %{notes} - flags_reminder: - flags_were_submitted: - one: "These flags were submitted over 1 hour ago." - other: "These flags were submitted over %{count} hours ago." - please_review: "Please review them." - post_number: "post" - how_to_disable: 'You can disable or change the frequency of this email reminder via the "notify about flags after" setting.' - subject_template: - one: "1 flag waiting to be handled" - other: "%{count} flags waiting to be handled" - queued_posts_reminder: subject_template: one: "[%{site_name}] 1 post waiting to be reviewed" diff --git a/spec/jobs/pending_flags_reminder_spec.rb b/spec/jobs/pending_flags_reminder_spec.rb index 4f325b0a8a..e73038c0fb 100644 --- a/spec/jobs/pending_flags_reminder_spec.rb +++ b/spec/jobs/pending_flags_reminder_spec.rb @@ -14,17 +14,17 @@ describe Jobs::PendingFlagsReminder do context "notify_about_flags_after is 48" do before { SiteSetting.stubs(:notify_about_flags_after).returns(48) } - it "doesn't send email when flags are less than 48 hours old" do + it "doesn't send message when flags are less than 48 hours old" do Fabricate(:flag, created_at: 47.hours.ago) PostAction.stubs(:flagged_posts_count).returns(1) - Email::Sender.any_instance.expects(:send).never + PostCreator.expects(:create).never described_class.new.execute({}) end - it "sends email when there is a flag older than 48 hours" do + it "sends message when there is a flag older than 48 hours" do Fabricate(:flag, created_at: 49.hours.ago) PostAction.stubs(:flagged_posts_count).returns(1) - Email::Sender.any_instance.expects(:send).once.returns(true) + PostCreator.expects(:create).once.returns(true) described_class.new.execute({}) end end From 9aa3653e2fd231c91988afca78324af89d54d77e Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Sun, 21 Feb 2016 02:26:28 -0800 Subject: [PATCH 121/140] Fix typo: are your sure --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c6bdcc24b1..08b557870b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2119,7 +2119,7 @@ en: is_disabled: "Restore is disabled in the site settings." label: "Restore" title: "Restore the backup" - confirm: "Are your sure you want to restore this backup?" + confirm: "Are you sure you want to restore this backup?" rollback: label: "Rollback" title: "Rollback the database to previous working state" From eb166e78b6c59d8579d13a2d285cec326824991c Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Sun, 21 Feb 2016 17:38:04 +0100 Subject: [PATCH 122/140] Don't try to import invalid websites --- app/models/user_profile.rb | 4 +++- script/import_scripts/base.rb | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index 32c0e2047c..38b04b74d5 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -1,8 +1,10 @@ class UserProfile < ActiveRecord::Base belongs_to :user, inverse_of: :user_profile + WEBSITE_REGEXP = /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,9}(([0-9]{1,5})?\/.*)?$)/ix + validates :bio_raw, length: { maximum: 3000 } - validates :website, format: { with: /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,9}(([0-9]{1,5})?\/.*)?$)/ix }, allow_blank: true + validates :website, format: { with: WEBSITE_REGEXP }, allow_blank: true validates :user, presence: true before_save :cook after_save :trigger_badges diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index b91296edaf..1ebdaee368 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -306,8 +306,8 @@ class ImportScripts::Base User.transaction do u.save! if bio_raw.present? || website.present? || location.present? - u.user_profile.bio_raw = bio_raw if bio_raw.present? - u.user_profile.website = website if website.present? + u.user_profile.bio_raw = bio_raw[0..2999] if bio_raw.present? + u.user_profile.website = website unless website.blank? || website !~ UserProfile::WEBSITE_REGEXP u.user_profile.location = location if location.present? u.user_profile.save! end From 5fdc0ebe8ab0010264435d1121cc3f14e13a8a79 Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Sun, 21 Feb 2016 14:12:32 -0800 Subject: [PATCH 123/140] Typo fix: "your sure" --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 08b557870b..6891ca5432 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2123,7 +2123,7 @@ en: rollback: label: "Rollback" title: "Rollback the database to previous working state" - confirm: "Are your sure you want to rollback the database to the previous working state?" + confirm: "Are you sure you want to rollback the database to the previous working state?" export_csv: user_archive_confirm: "Are you sure you want to download your posts?" From c4ec1d0fcf9f0b2b8cf8808c681cddc0f4ddb3b4 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Sun, 21 Feb 2016 23:11:52 +0100 Subject: [PATCH 124/140] FIX: Don't suggest invalid username --- lib/user_name_suggester.rb | 11 +++++++++-- spec/components/user_name_suggester_spec.rb | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/user_name_suggester.rb b/lib/user_name_suggester.rb index f39565941f..e809be0b41 100644 --- a/lib/user_name_suggester.rb +++ b/lib/user_name_suggester.rb @@ -42,14 +42,21 @@ module UserNameSuggester # 2. removes unallowed leading characters name.gsub!(/^\W+/, "") # 3. removes unallowed trailing characters - name.gsub!(/[^A-Za-z0-9]+$/, "") + name = remove_unallowed_trailing_characters(name) # 4. unify special characters name.gsub!(/[-_.]{2,}/, "_") name end + def self.remove_unallowed_trailing_characters(name) + name.gsub!(/[^A-Za-z0-9]+$/, "") + name + end + def self.rightsize_username(name) - name.ljust(User.username_length.begin, '1')[0, User.username_length.end] + name = name[0, User.username_length.end] + name = remove_unallowed_trailing_characters(name) + name.ljust(User.username_length.begin, '1') end end diff --git a/spec/components/user_name_suggester_spec.rb b/spec/components/user_name_suggester_spec.rb index 9d02951cbf..6046aa6ef7 100644 --- a/spec/components/user_name_suggester_spec.rb +++ b/spec/components/user_name_suggester_spec.rb @@ -93,6 +93,15 @@ describe UserNameSuggester do it 'should handle typical facebook usernames' do expect(UserNameSuggester.suggest('roger.nelson.3344913')).to eq('roger.nelson.33') end + + it 'removes underscore at the end of long usernames that get truncated' do + expect(UserNameSuggester.suggest('uuuuuuuuuuuuuu_u')).to_not end_with('_') + end + + it "adds number if it's too short after removing trailing underscore" do + User.stubs(:username_length).returns(8..8) + expect(UserNameSuggester.suggest('uuuuuuu_u')).to eq('uuuuuuu1') + end end end From 8a486d8cea224938be99353dafeef35006e968a7 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Sun, 21 Feb 2016 21:58:47 +0100 Subject: [PATCH 125/140] Allow importers to set empty names --- script/import_scripts/base.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index 1ebdaee368..63c55de590 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -278,7 +278,6 @@ class ImportScripts::Base avatar_url = opts.delete(:avatar_url) # Allow the || operations to work with empty strings '' - opts[:name] = nil if opts[:name].blank? opts[:username] = nil if opts[:username].blank? opts[:name] = User.suggest_name(opts[:email]) unless opts[:name] @@ -287,7 +286,8 @@ class ImportScripts::Base opts[:username].length > User.username_length.end || !User.username_available?(opts[:username]) || !UsernameValidator.new(opts[:username]).valid_format? - opts[:username] = UserNameSuggester.suggest(opts[:username] || opts[:name] || opts[:email]) + + opts[:username] = UserNameSuggester.suggest(opts[:username] || opts[:name].presence || opts[:email]) end opts[:email] = opts[:email].downcase opts[:trust_level] = TrustLevel[1] unless opts[:trust_level] From 4c0a40f2b0af51c370ee7312a1dd381d673b9254 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 22 Feb 2016 12:24:51 +1100 Subject: [PATCH 126/140] FIX: publish notification state when notifications are read (this clears green and blue bubbles) --- app/controllers/application_controller.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6dd3f81ade..b36e02ccd8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -154,7 +154,10 @@ class ApplicationController < ActionController::Base if notifications.present? notification_ids = notifications.split(",").map(&:to_i) - Notification.where(user_id: current_user.id, id: notification_ids).update_all(read: true) + count = Notification.where(user_id: current_user.id, id: notification_ids, read: false).update_all(read: true) + if count > 0 + current_user.publish_notifications_state + end cookies.delete('cn') end end From c852fb83d0db3aeeb98780fedc85a6c38f8c3eb2 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 22 Feb 2016 15:17:47 +0800 Subject: [PATCH 127/140] Upgrade Logster. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a614c28a89..0793f77cee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,7 +149,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.13) listen (0.7.3) - logster (1.0.1) + logster (1.1.1) loofah (2.0.3) nokogiri (>= 1.5.9) lru_redux (1.1.0) From 3142eb76dc2af77090a6eff25a0d899b1d3cb381 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 22 Feb 2016 15:55:48 +0800 Subject: [PATCH 128/140] Revert "FIX: find_by_attribute method in Rails 4.5 is case insensitive." This reverts commit 2af587005bc639f6b41bfafd7e4cbf5736222086. --- app/models/discourse_single_sign_on.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index 492e4a7041..894d545ada 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -81,7 +81,7 @@ class DiscourseSingleSignOn < SingleSignOn private def match_email_or_create_user(ip_address) - user = User.find_by(email: email) + user = User.find_by_email(email) try_name = name.blank? ? nil : name try_username = username.blank? ? nil : username From f6b1238d6c22b518a7ef3d2331ee56f5f869bcf1 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sat, 13 Feb 2016 16:13:49 -0800 Subject: [PATCH 129/140] reduce maximum_backups default from 7 to 5 --- config/site_settings.yml | 2 +- test/javascripts/helpers/site-settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index dbf80b3a3e..275dedf24e 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -871,7 +871,7 @@ backups: default: false maximum_backups: client: true - default: 7 + default: 5 shadowed_by_global: true automatic_backups_enabled: default: true diff --git a/test/javascripts/helpers/site-settings.js b/test/javascripts/helpers/site-settings.js index 7adde6c884..0beed5c979 100644 --- a/test/javascripts/helpers/site-settings.js +++ b/test/javascripts/helpers/site-settings.js @@ -80,7 +80,7 @@ Discourse.SiteSettingsOriginal = { "tos_accept_required":false, "faq_url":"", "allow_restore":false, - "maximum_backups":7, + "maximum_backups":5, "version_checks":true, "suppress_uncategorized_badge":true, "min_search_term_length":3, From 6a6e3a6a3a1629ae99f2bb4c7b6c0e2c25e856da Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 22 Feb 2016 01:17:50 -0800 Subject: [PATCH 130/140] FIX: add global hidden overflow on all topic bodies --- app/assets/stylesheets/common/base/discourse.scss | 1 - app/assets/stylesheets/common/base/topic-post.scss | 4 ++++ app/assets/stylesheets/desktop/topic-post.scss | 1 + app/assets/stylesheets/embed.css.scss | 1 - 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 8b8ab8040d..a0b23c263f 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -33,7 +33,6 @@ small { blockquote { @include post-aside; - overflow: hidden; clear: both; } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 0e2f63e5ce..5b3ecebf49 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -152,6 +152,10 @@ aside.quote { } .topic-body { + // this is necessary for ANYTHING that extends past the right edge of + // the post body, such as an image in a deeply nested list, image in + // a deeply nested blockquote, and so on.. you get the idea. + overflow: hidden; &.highlighted { background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); } diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 8744a17edf..8717c698c5 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -706,6 +706,7 @@ $topic-avatar-width: 45px; border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); padding: 12px $topic-body-width-padding 15px $topic-body-width-padding; } + .topic-avatar { border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); padding-top: 15px; diff --git a/app/assets/stylesheets/embed.css.scss b/app/assets/stylesheets/embed.css.scss index 3b1fd39740..826b4e284c 100644 --- a/app/assets/stylesheets/embed.css.scss +++ b/app/assets/stylesheets/embed.css.scss @@ -28,7 +28,6 @@ article.post { margin: 0 0 10px 0; background-color: dark-light-diff($primary, $secondary, 97%, -45%); border-left: 5px solid darken(dark-light-diff($primary, $secondary, 97%, -45%), 10%); - overflow: hidden; p { margin: 0 0 10px 0; } From 23063ea094607601d2e081baa3d0c05d11575e3f Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 22 Feb 2016 01:40:28 -0800 Subject: [PATCH 131/140] mobile needs different post body overflow handling --- app/assets/stylesheets/mobile/topic-post.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 05ae0a6646..496f1ef92a 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -352,6 +352,17 @@ span.post-count { padding: 15px 0; } +// mobile has no fixed width on topic-body so overflow: hidden causes problems +.topic-body { + overflow:inherit; +} + +// instead, for mobile we set overflow hidden on the cooked part of post body +// this prevents image overflow on deeply nested blockquotes, lists, etc +.cooked { + overflow: hidden; +} + .moderator .topic-body { background-color: dark-light-diff($highlight, $secondary, 70%, -80%); } From 66fa836d88012267419a56fe05098546cf012016 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 22 Feb 2016 01:53:51 -0800 Subject: [PATCH 132/140] we don't want a clear for notification options --- app/assets/stylesheets/mobile/topic.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index 75b772802f..2b421b8ae2 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -38,7 +38,6 @@ /* both blocks that appear under the standard post control buttons */ .notification-options, .pinned-options { - clear: both; float: left; margin-top: 0px; padding-top: 1px; From 2ab901bec46c67482d7dceba7e11b5d91504f5cf Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 22 Feb 2016 02:03:50 -0800 Subject: [PATCH 133/140] adjust bad styling on mobile user page --- app/assets/stylesheets/mobile/user.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 1b7d8c9f35..46198040cd 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -305,6 +305,7 @@ .primary-textual { float: left; + padding-left: 15px; a[href] { color: dark-light-diff($secondary, $primary, 75%, -10%); } @@ -329,16 +330,15 @@ } .controls { - width: 160px; - float: right; - text-align: right; + float: left; + padding-left: 15px; ul { list-style-type: none; margin: 0; } a { padding: 5px 10px; - width: 140px; + width: 120px; margin-bottom: 10px; } } From c9d19c946220a202ab6b5fa95a3453804899e946 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 22 Feb 2016 02:18:34 -0800 Subject: [PATCH 134/140] tighten up mobile profile image size --- app/assets/stylesheets/mobile/user.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 46198040cd..7725172431 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -67,9 +67,8 @@ } .profile-image { - height: 150px; + height: 25px; width: 100%; - background-size: cover; } @@ -209,8 +208,7 @@ } .about { - background-size: cover; - background: dark-light-diff($primary, $secondary, 0%, -75%) center center; + background: dark-light-diff($primary, $secondary, 0%, -75%) center; width: 100%; margin-bottom: 10px; overflow: hidden; From 5f747a74a174226d492cb372f917a45d124d014e Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 22 Feb 2016 16:05:40 +0530 Subject: [PATCH 135/140] Update onebox version --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0793f77cee..2bc7adb15d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -211,7 +211,7 @@ GEM omniauth-twitter (1.2.1) json (~> 1.3) omniauth-oauth (~> 1.1) - onebox (1.5.34) + onebox (1.5.35) moneta (~> 0.8) multi_json (~> 1.11) mustache From 4d981cec53aa80813d50018c2ddab6e9947a1d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 22 Feb 2016 12:57:24 +0100 Subject: [PATCH 136/140] FIX: don't try to optimize large PNGs (takes too much time) --- app/controllers/uploads_controller.rb | 34 +++++++++++++++------------ app/models/upload.rb | 20 ++++++++++++---- spec/models/upload_spec.rb | 1 - 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 65be0cea90..f91f70763a 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -51,13 +51,16 @@ class UploadsController < ApplicationController render nothing: true, status: 404 end + MAXIMUM_UPLOAD_SIZE ||= 10.megabytes + DOWNSIZE_RATIO ||= 0.8 + def create_upload(type, file, url) begin # ensure we have a file if file.nil? # API can provide a URL if url.present? && is_api? - tempfile = FileHelper.download(url, 10.megabytes, "discourse-upload-#{type}") rescue nil + tempfile = FileHelper.download(url, MAXIMUM_UPLOAD_SIZE, "discourse-upload-#{type}") rescue nil filename = File.basename(URI.parse(url).path) end else @@ -69,20 +72,21 @@ class UploadsController < ApplicationController return { errors: I18n.t("upload.file_missing") } if tempfile.nil? # allow users to upload (not that) large images that will be automatically reduced to allowed size - uploaded_size = File.size(tempfile.path) - if SiteSetting.max_image_size_kb > 0 && FileHelper.is_image?(filename) && uploaded_size > 0 && uploaded_size < 10.megabytes - attempt = 2 - allow_animation = type == "avatar" ? SiteSetting.allow_animated_avatars : SiteSetting.allow_animated_thumbnails - while attempt > 0 - downsized_size = File.size(tempfile.path) - break if downsized_size > uploaded_size - break if downsized_size < SiteSetting.max_image_size_kb.kilobytes - image_info = FastImage.new(tempfile.path) rescue nil - w, h = *(image_info.try(:size) || [0, 0]) - break if w == 0 || h == 0 - dimensions = "#{(w * 0.8).floor}x#{(h * 0.8).floor}" - OptimizedImage.downsize(tempfile.path, tempfile.path, dimensions, filename: filename, allow_animation: allow_animation) - attempt -= 1 + if SiteSetting.max_image_size_kb > 0 && FileHelper.is_image?(filename) + uploaded_size = File.size(tempfile.path) + if 0 < uploaded_size && uploaded_size < MAXIMUM_UPLOAD_SIZE && Upload.should_optimize?(tempfile.path) + attempt = 2 + allow_animation = type == "avatar" ? SiteSetting.allow_animated_avatars : SiteSetting.allow_animated_thumbnails + while attempt > 0 + downsized_size = File.size(tempfile.path) + break if uploaded_size < downsized_size || downsized_size < SiteSetting.max_image_size_kb.kilobytes + image_info = FastImage.new(tempfile.path) rescue nil + w, h = *(image_info.try(:size) || [0, 0]) + break if w == 0 || h == 0 + dimensions = "#{(w * DOWNSIZE_RATIO).floor}x#{(h * DOWNSIZE_RATIO).floor}" + OptimizedImage.downsize(tempfile.path, tempfile.path, dimensions, filename: filename, allow_animation: allow_animation) + attempt -= 1 + end end end diff --git a/app/models/upload.rb b/app/models/upload.rb index 6c69373018..49480639d4 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -73,8 +73,8 @@ class Upload < ActiveRecord::Base w = svg["width"].to_i h = svg["height"].to_i else - # fix orientation first (but not for GIFs) - fix_image_orientation(file.path) unless filename =~ /\.GIF$/i + # fix orientation first + fix_image_orientation(file.path) if should_optimize?(file.path) # retrieve image info image_info = FastImage.new(file) rescue nil w, h = *(image_info.try(:size) || [0, 0]) @@ -107,8 +107,8 @@ class Upload < ActiveRecord::Base end end - # optimize image (but not for GIFs) - if filename !~ /\.GIF$/i + # optimize image (except GIFs and large PNGs) + if should_optimize?(file.path) ImageOptim.new.optimize_image!(file.path) rescue nil # update the file size filesize = File.size(file.path) @@ -163,6 +163,18 @@ class Upload < ActiveRecord::Base end end + LARGE_PNG_SIZE ||= 3.megabytes + + def self.should_optimize?(path) + # don't optimize GIFs + return false if path =~ /\.gif$/i + return true if path !~ /\.png$/i + image_info = FastImage.new(path) rescue nil + w, h = *(image_info.try(:size) || [0, 0]) + # don't optimize large PNGs + w > 0 && h > 0 && w * h < LARGE_PNG_SIZE + end + def self.is_dimensionless_image?(filename, width, height) FileHelper.is_image?(filename) && (width.blank? || width == 0 || height.blank? || height == 0) end diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index b67196ec6d..deddf2b795 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -62,7 +62,6 @@ describe Upload do end it "computes width & height for images" do - FastImage.any_instance.expects(:size).returns([100, 200]) ImageSizer.expects(:resize) image.expects(:rewind).twice Upload.create_for(user_id, image, image_filename, image_filesize) From c2caf90de6c6be14165e4728ee4edf8f6308888e Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 22 Feb 2016 18:28:02 +0530 Subject: [PATCH 137/140] FIX: RSS feed must have unique GUID --- app/views/list/list.rss.erb | 2 +- app/views/posts/latest.rss.erb | 2 +- app/views/topics/show.rss.erb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/list/list.rss.erb b/app/views/list/list.rss.erb index b1eb41f103..7cc954ec2a 100644 --- a/app/views/list/list.rss.erb +++ b/app/views/list/list.rss.erb @@ -30,7 +30,7 @@ <%= topic.pinned_at ? 'Yes' : 'No' %> <%= topic.closed ? 'Yes' : 'No' %> <%= topic.archived ? 'Yes' : 'No' %> - topic-<%= topic.id %> + <%= Discourse.current_hostname %>-topic-<%= topic.id %> <%= topic.title %> <% end %> diff --git a/app/views/posts/latest.rss.erb b/app/views/posts/latest.rss.erb index 7cfb0a8e1d..8f97eff3f0 100644 --- a/app/views/posts/latest.rss.erb +++ b/app/views/posts/latest.rss.erb @@ -14,7 +14,7 @@ ]]> <%= Discourse.base_url + post.url %> <%= post.created_at.rfc2822 %> - post-<%= post.id %> + <%= Discourse.current_hostname %>-post-<%= post.id %> <% end %> diff --git a/app/views/topics/show.rss.erb b/app/views/topics/show.rss.erb index e49890e2d0..aa7d08c4a8 100644 --- a/app/views/topics/show.rss.erb +++ b/app/views/topics/show.rss.erb @@ -30,7 +30,7 @@ ]]> <%= post_url %> <%= post.created_at.rfc2822 %> - post-<%= post.topic_id %>-<%= post.post_number %> + <%= Discourse.current_hostname %>-post-<%= post.topic_id %>-<%= post.post_number %> <%= @topic_view.title %> <% end %> From 6763a9923ade32e6a8638f44f210b82b0f7d00c7 Mon Sep 17 00:00:00 2001 From: Joe Buhlig Date: Fri, 12 Feb 2016 11:09:29 -0600 Subject: [PATCH 138/140] Added tertiary color to digest Added hash to color in helper Added anchor_color to topic and site name links Styled the unsubscribe link --- app/helpers/user_notifications_helper.rb | 4 ++-- app/mailers/user_notifications.rb | 1 + app/views/user_notifications/digest.html.erb | 14 +++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/helpers/user_notifications_helper.rb b/app/helpers/user_notifications_helper.rb index 51a5e7ffea..24ca2ef221 100644 --- a/app/helpers/user_notifications_helper.rb +++ b/app/helpers/user_notifications_helper.rb @@ -28,8 +28,8 @@ module UserNotificationsHelper logo_url end - def html_site_link - "#{@site_name}" + def html_site_link(color) + "#{@site_name}" end def first_paragraph_from(html) diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 5172942781..a338e607f2 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -62,6 +62,7 @@ class UserNotifications < ActionMailer::Base @site_name = SiteSetting.email_prefix.presence || SiteSetting.title @header_color = ColorScheme.hex_for_name('header_background') + @anchor_color = ColorScheme.hex_for_name('tertiary') @last_seen_at = short_date(@user.last_seen_at || @user.created_at) # A list of topics to show the user diff --git a/app/views/user_notifications/digest.html.erb b/app/views/user_notifications/digest.html.erb index 959562b2ea..304250e12f 100644 --- a/app/views/user_notifications/digest.html.erb +++ b/app/views/user_notifications/digest.html.erb @@ -1,7 +1,7 @@
    - - - <%= post.user.username %> - <%- if show_name_on_post(post) %> - <%= post.user.name %> - <% end %> - <%- if post.user.title.present? %> - <%= post.user.title %> - <% end %> -
    - <%= l post.created_at, format: :short_no_year %> + + + + + +
    + + + <%= post.user.username %> + <%- if show_name_on_post(post) %> + <%= post.user.name %> + <% end %> + <%- if post.user.title.present? %> + <%= post.user.title %> + <% end %> +
    + <%= l post.created_at, format: :short_no_year %> +
    {{email.subject}} - {{email.error}} + {{email.error}}
    {{l.to_address}} {{l.email_type}}{{l.reply_key}} + {{#if l.post_url}} + {{l.reply_key}} + {{else}} + {{l.reply_key}} + {{/if}} +
    {{i18n 'admin.email.logs.none'}}
    {{l.to_address}} {{l.email_type}}{{l.skipped_reason}} + {{#if l.post_url}} + {{l.skipped_reason}} + {{else}} + {{l.skipped_reason}} + {{/if}} +
    {{i18n 'admin.email.logs.none'}}
    - + <%- if logo_url.blank? %> <%= SiteSetting.title %> <%- else %> @@ -11,7 +11,7 @@
    - <%= raw(t 'user_notifications.digest.why', site_link: html_site_link, last_seen_at: @last_seen_at) %> + <%= raw(t 'user_notifications.digest.why', site_link: html_site_link(@anchor_color), last_seen_at: @last_seen_at) %> <%- if @featured_topics.present? %>
    @@ -20,7 +20,7 @@ <%- @featured_topics.each_with_index do |t, i| %> @@ -43,7 +43,7 @@ <%- @new_topics.each do |t| %>
    • - <%= raw unescape_emoji(t.title) %> + <%= raw unescape_emoji(t.title) %> <%= t.posts_count %> <%= category_badge(t.category, inline_style: true, absolute_url: true) %>
    • @@ -58,7 +58,7 @@
      <%- @new_by_category.first(10).each do |c| %> - <%= c[0].name %> <%= c[1] %> + <%= c[0].name %> <%= c[1] %> <%- end %>
      @@ -72,6 +72,6 @@ From 7e39619bc9196becee0ee77918e56fdc0c3f1678 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 22 Feb 2016 11:10:12 -0500 Subject: [PATCH 139/140] Update translations --- config/locales/client.ar.yml | 52 ++ config/locales/client.bs_BA.yml | 10 +- config/locales/client.de.yml | 26 +- config/locales/client.es.yml | 4 + config/locales/client.fi.yml | 6 +- config/locales/client.fr.yml | 4 + config/locales/client.it.yml | 2 +- config/locales/client.ja.yml | 40 ++ config/locales/client.ko.yml | 40 +- config/locales/client.pt_BR.yml | 27 + config/locales/client.ro.yml | 187 +++++- config/locales/client.vi.yml | 556 +++++++++++++++++- config/locales/client.zh_CN.yml | 77 ++- config/locales/server.ar.yml | 14 + config/locales/server.de.yml | 48 +- config/locales/server.es.yml | 12 +- config/locales/server.fi.yml | 1 + config/locales/server.fr.yml | 112 ++++ config/locales/server.ja.yml | 9 +- config/locales/server.pt_BR.yml | 7 + config/locales/server.ru.yml | 49 +- config/locales/server.sk.yml | 36 +- config/locales/server.vi.yml | 127 +++- config/locales/server.zh_CN.yml | 8 +- plugins/poll/config/locales/client.ja.yml | 6 + plugins/poll/config/locales/client.vi.yml | 16 +- plugins/poll/config/locales/server.ja.yml | 4 + plugins/poll/config/locales/server.vi.yml | 14 +- public/403.vi.html | 1 - public/422.vi.html | 1 - public/500.vi.html | 1 - public/503.vi.html | 1 - .../lib/discourse_imgur/locale/server.vi.yml | 12 +- 33 files changed, 1416 insertions(+), 94 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 0dd9c71b55..c89836ac16 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -176,6 +176,8 @@ ar: few: "بعد %{count} سنوات" many: "بعد %{count} سنة" other: "بعد %{count} سنة" + previous_month: 'الشهر السابق' + next_month: 'الشهر التالي' share: topic: 'شارك رابط هذا الموضوع' post: 'الموضوع رقم %{postNumber}' @@ -186,6 +188,8 @@ ar: email: 'شارك هذا الرابط في بريد إلكتروني' action_codes: split_topic: "قسم هذا الموضوع في %{when}" + invited_user: "دعوة %{who} %{when}" + removed_user: "ازالة %{who} %{when}" autoclosed: enabled: 'أُغلق في %{when}' disabled: 'فُتح في %{when}' @@ -257,6 +261,7 @@ ar: other: "{{count}} حرف" suggested_topics: title: "مواضيع مقترحة" + pm_title: "رسائلة مقترحة " about: simple_title: "نبذة" title: "عن %{title}" @@ -533,6 +538,7 @@ ar: not_supported: "عذراً , الإشعارات غير مدعومة على هذا المتصفح " perm_default: "تفعيل الإشعارات" perm_denied_btn: "الصلاحيات ممنوعة " + perm_denied_expl: "لقد قمت بالغاء الاشعارات . قم بتفعيل الاشعارات عن طريق اعدادات االمتصفح" disable: "إيقاف الإشعارات " currently_enabled: "( مفعل مسبقاً )" enable: "تفعيل الإشعارات" @@ -586,6 +592,7 @@ ar: groups: "مجموعاتي" bulk_select: "إختيار رسائل" move_to_inbox: "الذهاب إلى الرسائل الواردة" + move_to_archive: "الارشيف" failed_to_move: "فشل في نقل الرسائل المحددة (ربما يكون اتصالك ضعيفاً)" select_all: "إختيار الكل" change_password: @@ -752,6 +759,20 @@ ar: same_as_email: "كلمة المرور مطابقة للبريد الإليكتروني." ok: "كلمة المرور هذة تعتبر جيدة." instructions: "على الاقل %{count} حرف" + summary: + title: "ملخص" + stats: "احصائيات" + topic_count: "مواضيع منشئة" + post_count: "مشاركات منشئة" + likes_given: "اجابات معطاة" + likes_received: "اعجابات مستقبلة" + days_visited: "ايام الزياره" + posts_read_count: "قراء المواضيع" + top_topics: "افضل المواضيع" + top_badges: "افضل الاوسمه" + more_topics: "المزيد من المواضيع" + more_replies: "المزيد من الردود" + more_badges: "المزيد من الاوسمه" associated_accounts: "حساب مرتبط" ip_address: title: "أخر عنوان أيبي" @@ -794,6 +815,7 @@ ar: logout: "تم تسجيل خروجك" refresh: "تحديث" read_only_mode: + enabled: "الموقع في وضع القراءه فقط. الرجاء الاستمرار بالتصفح , لكن الردود و الاعجابات و الخصائص الاخرى معطله للان ." login_disabled: "تسجيل الدخول معطل لأن الموقع في خالة القراءة فقط" too_few_topics_and_posts_notice: "دعونا الحصول على هذه المناقشة بدأت! يوجد حاليا%{currentTopics} / %{requiredTopics} المواضيع و %{currentPosts} / %{requiredPosts} المشاركات. الزوار الجدد بحاجة إلى بعض الأحاديث لقراءة والرد على." too_few_topics_notice: "دعونا الحصول على هذه المناقشة التي! وهناك حاليا %{currentTopics} / %{requiredTopics} المواضيع. الزوار الجديدة بحاجة إلى بعض الأحاديث قراءة والرد عليها." @@ -912,6 +934,7 @@ ar: ctrl: 'التحكم' alt: 'Alt' composer: + emoji: "الرموز التعبيرية :)" more_emoji: "أكثر..." options: "خيارات" whisper: "همس" @@ -1021,6 +1044,7 @@ ar: moved_post: "مشاركتك نقلت بواسطة" linked: "رابط لمشاركتك" granted_badge: "تم منح الوسام" + group_message_summary: "الرسائل في صندوق رسائل المجموعه" popup: mentioned: '{{username}} أشار لك في "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} ذكرك في "{{topic}}" - {{site_title}}' @@ -1083,6 +1107,7 @@ ar: dismiss_read: "تجاهل المشاركات غير المقروءة" dismiss_button: "تجاهل..." dismiss_tooltip: "تجاهل فقط المشاركات الجديدة او توقف عن تتبع المواضيع" + also_dismiss_topics: "التوقف عن متابعه المواضيع حتي لا تظهر كغير مقروء مره اخرى " dismiss_new: "إخفاء الجديد" toggle: "إيقاف/تشغيل الاختيار المتعدد للمواضيع" actions: "عمليات تنفذ دفعة واحدة" @@ -1339,6 +1364,7 @@ ar: success: "لقد دعونا ذلك المستخدم للمشاركة في هذه الرسالة." error: "للأسف, حدثت مشكلة في دعوة المستخدم" group_name: "اسم المجموعة" + controls: "خصائص الموضوع" invite_reply: title: 'دعوة' username_placeholder: "اسم المستخدم" @@ -2049,6 +2075,7 @@ ar: refresh_report: "تحديث التقرير " start_date: "تاريخ البدء" end_date: "تاريخ الإنتهاء" + groups: "جميع الفئات" commits: latest_changes: "آخر تغيير: يرجى التحديث" by: "بواسطة" @@ -2351,13 +2378,17 @@ ar: name: 'ويكي' description: "اللون الأساسي المستخدم كخلفية لمشاركات الويكي." email: + title: "رسائل البريد الالكتروني" settings: "اعدادات" + templates: "نماذج" preview_digest: "ملخص المعاينة." sending_test: "إرسال بريد إلكتروني للتجربة..." error: "خطأ - %{server_error}" test_error: "حدث خطأ أثناء إرسال رسالة تجريبية. الرجاء فحص إعدادات البريد الإلكتروني و التأكد من أن الاستضافة لا تمنع مرور البريد الإلكتروني والمحاولة مرة أخرى." sent: "تم الإرسال" skipped: "تم التجاوز" + received: "وارد" + rejected: "مرفوض" sent_at: "أرسلت في" time: "الوقت" user: "المستخدم" @@ -2375,6 +2406,19 @@ ar: last_seen_user: "آخر مستخدم تواجد:" reply_key: "مفتاح الرد" skipped_reason: "تجاوز السبب" + incoming_emails: + from_address: "من" + to_addresses: "الى" + cc_addresses: "Cc" + subject: "موضوع" + error: "خطأ" + none: "لا يوجد بريد وارد" + filters: + from_placeholder: "from@example.com" + to_placeholder: "to@example.com" + cc_placeholder: "cc@example.com" + subject_placeholder: "موضوع..." + error_placeholder: "خطأ" logs: none: "لا يوجد سجلات." filters: @@ -2438,6 +2482,10 @@ ar: change_category_settings: "تغيير إعدادات الفئة" delete_category: "حذف الفئة" create_category: "أنشئ فئة" + block_user: "حظر" + unblock_user: "رفع الحظر" + grant_admin: "منح صلاحيات ادارية" + revoke_admin: "سحب الصلاحيات الادارية" screened_emails: title: "عناوين بريد إلكتروني محجوبة." description: "عندما تتم محاول انشاء حساب جديد, سيتم التحقق من قائمة البريد الالكتروني وسيتم حظر التسجيل لهذا البريد واتخاذ اي اجراء متبع" @@ -2631,6 +2679,8 @@ ar: deactivate_failed: "حدث خطأ عند تعطيل هذا المستخدم." unblock_failed: 'حدث خطأ عند الغاء حظر هذا المستخدم.' block_failed: 'حدث خطأ عند حظر هذا المستخدم.' + block_confirm: 'هل انت متأكد من حظر هذا المستخدم؟ لن يستطيع انشاء مواضيع او ردود جديدة' + block_accept: 'نعم, حظر هذا المستخدم' deactivate_explanation: "المستخدم الغير نشط يحب أن يتأكد من البريد الالكتروني" suspended_explanation: "المستخدم الموقوف لايملك صلاحية تسجيل الدخول" block_explanation: "المستخدم الموقوف لايستطيع أن يشارك" @@ -2705,6 +2755,7 @@ ar: confirm: 'تأكيد' dropdown: "القائمة المنسدلة" site_text: + description: "يمكنك تخصيص اي نص في مدونتك . الرجاء البدء بالبحث في الاسفل:" search: "ابحث عن النص الذي تريد تعديله" title: 'محتوى النص' edit: 'تعديل' @@ -2915,6 +2966,7 @@ ar: mark_tracking: 'm, t تابع الموضوع' mark_watching: 'm, w شاهد الموضوع' badges: + more_with_badge: "اخرون بنفس الدرع" title: أوسمة allow_title: "يمكن استخدامه كعنوان" multiple_grant: "يمكن منحه عدة مرات. " diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index faee9cf521..d2dc34c974 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -94,12 +94,12 @@ bs_BA: few: "Prije par godina" other: "%{count} godine/a prije" share: - topic: 'podjeli link ka ovoj temi' - post: 'podjeli link ka ovom postu #%{postNumber}' + topic: 'podijeli link ka ovoj temi' + post: 'podijeli link ka ovom postu #%{postNumber}' close: 'zatvori' - twitter: 'podjeli link na Twitteru' - facebook: 'podjeli link na Facebooku' - google+: 'podjeli link na Google+' + twitter: 'podijeli link na Twitteru' + facebook: 'podijeli link na Facebook-u' + google+: 'podijeli link na Google+' email: 'pošalji ovaj link na email' topic_admin_menu: "topic admin actions" edit: 'izmjeni naslov i kategoriju ove teme' diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 5062acb46b..30a9605535 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -100,6 +100,8 @@ de: x_years: one: "ein Jahr später" other: "%{count} Jahre später" + previous_month: 'Vormonat' + next_month: 'Nächster Monat' share: topic: 'Teile einen Link zu diesem Thema' post: 'Beitrag #%{postNumber}' @@ -188,6 +190,7 @@ de: other: "{{count}} Zeichen" suggested_topics: title: "Vorgeschlagene Themen" + pm_title: "Vorgeschlagene Nachrichten" about: simple_title: "Über uns" title: "Über %{title}" @@ -428,7 +431,7 @@ de: not_supported: "Dieser Browser unterstützt leider keine Benachrichtigungen." perm_default: "Benachrichtigungen einschalten" perm_denied_btn: "Zugriff verweigert" - perm_denied_expl: "Der Zugriff auf Benachrichtigungen wurde verweigert. Verwende Deinen Browser um Benachrichtigungen zu aktivieren. Anschließend klick auf die Schaltfläche. (Desktop: das Symbol ganz links in der Adressfläche. Mobil: \"Seiten Info\".)" + perm_denied_expl: "Du hast Benachrichtigungen blockiert. Aktiviere die Benachrichtigungen über deine Browser Einstellungen." disable: "Benachrichtigungen deaktivieren" currently_enabled: "(derzeit aktiviert)" enable: "Benachrichtigungen aktivieren" @@ -698,6 +701,7 @@ de: logout: "Du wurdest abgemeldet." refresh: "Aktualisieren" read_only_mode: + enabled: "Diese Seite ist im Nur-Lesen Modus. Du kannst weiterhin die Seite lesen, aber antworten, liken und andere Aktionen sind deaktiviert." login_disabled: "Die Anmeldung ist deaktiviert während sich die Website im Nur-Lesen-Modus befindet." too_few_topics_and_posts_notice: "Lass' die Diskussionen starten! Es existieren bisher %{currentTopics} von %{requiredTopics} benötigten Themen und %{currentPosts} von %{requiredPosts} benötigten Beiträgen. Neue Besucher benötigen bestehende Konversationen, die sie lesen und auf die sie antworten können." too_few_topics_notice: "Lass' die Diskussionen starten! Es existieren bisher %{currentTopics} von %{requiredTopics} benötigten Themen. Neue Besucher benötigen bestehende Konversationen, die sie lesen und auf die sie antworten können." @@ -909,6 +913,9 @@ de: moved_post: "

      {{username}} hat {{description}} verschoben

      " linked: "

      {{username}} {{description}}

      " granted_badge: "

      Abzeichen '{{description}}' erhalten

      " + group_message_summary: + one: "

      Eine Nachricht in deinem {{group_name}} Postfach

      " + other: "

      {{count}} Nachrichten in deinem {{group_name}} Postfach

      " alt: mentioned: "Erwähnt von" quoted: "Zitiert von" @@ -923,6 +930,7 @@ de: moved_post: "Dein Beitrag wurde verschoben von" linked: "Link zu deinem Beitrag" granted_badge: "Abzeichen erhalten" + group_message_summary: "Nachrichten in dem Gruppenpostfach" popup: mentioned: '{{username}} hat dich in "{{topic}}" - {{site_title}} erwähnt' group_mentioned: '{{username}} hat dich in "{{topic}}" - {{site_title}} erwähnt' @@ -1752,6 +1760,7 @@ de: refresh_report: "Bericht aktualisieren" start_date: "Startdatum" end_date: "Enddatum" + groups: "Alle Gruppen" commits: latest_changes: "Letzte Änderungen: bitte häufig updaten!" by: "von" @@ -2043,6 +2052,8 @@ de: test_error: "Es gab ein Problem beim Senden der Test-E-Mail. Bitte überprüfe nochmals deine E-Mail-Einstellungen, stelle sicher dass dein Anbieter keine E-Mail-Verbindungen blockiert und probiere es erneut." sent: "Gesendet" skipped: "Übersprungen" + received: "Empfangen" + rejected: "Abgelehnt" sent_at: "Gesendet am" time: "Zeit" user: "Benutzer" @@ -2066,6 +2077,7 @@ de: cc_addresses: "Cc" subject: "Betreff" error: "Fehler" + none: "Keine eingehenden E-Mails gefunden." filters: from_placeholder: "von@example.com" to_placeholder: "an@example.com" @@ -2137,6 +2149,10 @@ de: create_category: "Kategorie erstellen" block_user: "Benutzer blockieren" unblock_user: "Blockierung von Benutzer aufheben" + grant_admin: "Administration gewähren" + revoke_admin: "Administration entziehen" + grant_moderation: "Moderation gewähren" + revoke_moderation: "Moderation entziehen" screened_emails: title: "Gefilterte E-Mails" description: "Wenn jemand ein Konto erstellt, werden die folgenden E-Mail-Adressen überprüft und es wird die Anmeldung blockiert oder eine andere Aktion ausgeführt." @@ -2238,6 +2254,7 @@ de: moderator: "Moderator?" admin: "Administrator?" blocked: "Geblockt?" + staged: "Insziniert?" show_admin_profile: "Administration" edit_title: "Titel bearbeiten" save_title: "Titel speichern" @@ -2302,9 +2319,12 @@ de: deactivate_failed: "Beim Deaktivieren des Benutzers ist ein Fehler aufgetreten." unblock_failed: 'Beim Aufheben der Blockierung des Benutzers ist ein Fehler aufgetreten.' block_failed: 'Beim Blocken des Benutzers ist ein Fehler aufgetreten.' + block_confirm: 'Bust du sicher, dass du diesen Benutzer blockieren willst? Sie werden keine Möglichkeit mehr haben, Themen oder Beiträge zu erstellen.' + block_accept: 'Ja, den Nutzer blockieren.' deactivate_explanation: "Ein deaktivierter Benutzer muss seine E-Mail-Adresse erneut bestätigen." suspended_explanation: "Ein gesperrter Benutzer kann sich nicht anmelden." block_explanation: "Ein geblockter Benutzer kann keine Themen erstellen oder Beiträge veröffentlichen." + stage_explanation: "Ein inszenierter Nutzer kann nur via E-Mail zu gewissen Themen beitragen." trust_level_change_failed: "Beim Wechsel der Vertrauensstufe ist ein Fehler aufgetreten." suspend_modal_title: "Benutzer sperren" trust_level_2_users: "Benutzer mit Vertrauensstufe 2" @@ -2583,6 +2603,10 @@ de: mark_tracking: 'm, t Thema verfolgen' mark_watching: 'm, w Thema beobachten' badges: + earned_n_times: + one: "Einmal dieses Abzeichen erhalten" + other: "%{count} mal dieses Abzeichen erhalten" + more_with_badge: "Andere mit diesem Abzeichen" title: Abzeichen allow_title: "kann als Titel verwendet werden" multiple_grant: "kann mehrfach verliehen werden" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 0be0a1026c..b65b513439 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -100,6 +100,8 @@ es: x_years: one: "%{count} año después" other: "%{count} años después" + previous_month: 'Anterior mes' + next_month: 'Próximo mes' share: topic: 'comparte un enlace a este tema' post: 'post #%{postNumber}' @@ -188,6 +190,7 @@ es: other: "{{count}} caracteres" suggested_topics: title: "Temas Sugeridos" + pm_title: "Mensajes sugeridos" about: simple_title: "Acerca de" title: "Sobre %{title}" @@ -1759,6 +1762,7 @@ es: refresh_report: "Actualizar reporte" start_date: "Desde fecha" end_date: "Hasta fecha" + groups: "Todos los grupos" commits: latest_changes: "Cambios recientes: ¡actualiza a menudo!" by: "por" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 4ebf5199d8..e3f9a11817 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -100,6 +100,8 @@ fi: x_years: one: "1 vuosi myöhemmin" other: "%{count} vuotta myöhemmin" + previous_month: 'Edellinen kuukausi' + next_month: 'Seuraava kuukausi' share: topic: 'jaa linkki tähän ketjuun' post: '%{postNumber}. viesti' @@ -188,6 +190,7 @@ fi: other: "{{count}} merkkiä" suggested_topics: title: "Suositellut ketjut" + pm_title: "Suositellut viestit" about: simple_title: "Tietoja" title: "Tietoja sivustosta %{title}" @@ -1053,7 +1056,7 @@ fi: login_required: "Sinun täytyy kirjautua sisään nähdäksesi tämän ketjun." server_error: title: "Ketjun lataaminen epäonnistui" - description: "Pahoittelut, ketjun lataaminen epäonnistui. Kyse saattaa olla yhteysongelmsta. Kokeile sivun lataamista uudestaan ja jos ongelma jatkuu, ota yhteyttä." + description: "Pahoittelut, ketjun lataaminen epäonnistui. Kyse saattaa olla yhteysongelmasta. Kokeile sivun lataamista uudestaan ja jos ongelma jatkuu, ota yhteyttä." not_found: title: "Ketjua ei löytynyt" description: "Pahoittelut, ketjua ei löytynyt. Ehkäpä valvoja on siirtänyt sen muualle?" @@ -1759,6 +1762,7 @@ fi: refresh_report: "Päivitä raportti" start_date: "Alkupäivämäärä" end_date: "Loppupäivämäärä" + groups: "Kaikki ryhmät" commits: latest_changes: "Viimeisimmät muutokset: päivitä usein!" by: "käyttäjältä" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 3600bedc89..9b5f0f066b 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -100,6 +100,8 @@ fr: x_years: one: "1 année plus tard" other: "%{count} années plus tard" + previous_month: 'Mois précédent' + next_month: 'Mois suivant' share: topic: 'partager ce sujet' post: 'message #%{postNumber}' @@ -188,6 +190,7 @@ fr: other: "{{count}} caractères" suggested_topics: title: "Sujets similaires" + pm_title: "Messages Proposés" about: simple_title: "A propos" title: "A propos de %{title}" @@ -1763,6 +1766,7 @@ fr: refresh_report: "Actualiser le rapport" start_date: "Date de début" end_date: "Date de fin" + groups: "Tous les groupes" commits: latest_changes: "Dernières modifications: merci de mettre à jour régulièrement!" by: "par" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index d76d658f01..01638e495a 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -109,7 +109,7 @@ it: google+: 'Condividi questo link su Google+' email: 'invia questo collegamento via email' action_codes: - split_topic: "suddividi questo argomento %{when}" + split_topic: "ha separato questo argomento %{when}" invited_user: "Invitato %{who} %{when}" removed_user: "rimosso %{who} %{when}" autoclosed: diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 9ea38f58cc..dd1989681a 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -81,6 +81,8 @@ ja: other: "%{count} 月後" x_years: other: "%{count} 年後" + previous_month: '前月' + next_month: '来月' share: topic: 'このトピックのリンクをシェアする' post: 'ポスト #%{postNumber}' @@ -91,6 +93,19 @@ ja: email: 'メールでこのリンクを送る' topic_admin_menu: "トピック管理" emails_are_disabled: "全てのメールアドレスの送信が管理者によって無効化されています。全ての種類のメール通知は行われません" + s3: + regions: + us_east_1: "US East (N. Virginia)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" + us_gov_west_1: "AWS GovCloud (US)" + eu_west_1: "EU (Ireland)" + eu_central_1: "EU (Frankfurt)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" + ap_northeast_1: "Asia Pacific (Tokyo)" + ap_northeast_2: "Asia Pacific (Seoul)" + sa_east_1: "South America (Sao Paulo)" edit: 'このトピックのタイトルとカテゴリを編集' not_implemented: "申し訳ありませんが、この機能はまだ実装されていません" no_value: "いいえ" @@ -282,6 +297,7 @@ ja: category: "カテゴリ" reorder: title: "カテゴリを並び替える" + apply_all: "適用" posts: "ポスト" topics: "トピック" latest: "最新ポスト" @@ -376,6 +392,8 @@ ja: warnings_received: "注意" messages: all: "すべて" + inbox: "インボックス" + select_all: "全てを選択する" change_password: success: "(メールを送信しました)" in_progress: "(メールを送信中)" @@ -474,6 +492,13 @@ ja: auto_track_options: never: "トラックしない" immediately: "今すぐ" + after_30_seconds: "30秒後" + after_1_minute: "1分後" + after_2_minutes: "2分後" + after_3_minutes: "3分後" + after_4_minutes: "4分後" + after_5_minutes: "5分後" + after_10_minutes: "10分後" invited: search: "招待履歴を検索するためにキーワードを入力してください..." title: "招待" @@ -646,6 +671,7 @@ ja: twitter: "Twitter" emoji_one: "Emoji One" composer: + options: "オプション" add_warning: "これは公式の警告です。" posting_not_on_topic: "回答したいトピックはどれですか?" saving_draft_tip: "保存中..." @@ -674,6 +700,7 @@ ja: edit_reason_placeholder: "編集する理由は何ですか?" show_edit_reason: "(編集理由を追加)" view_new_post: "新規ポストを見る。" + saving: "保存中" saved: "保存完了!" saved_draft: "編集中の投稿があります。選択すると再開します" uploading: "アップロード中..." @@ -702,6 +729,7 @@ ja: hr_title: "水平線" help: "Markdown 編集のヘルプ" toggler: "編集パネルの表示/非表示" + modal_cancel: "キャンセル" admin_options_title: "このトピックの詳細設定" auto_close: label: "このトピックを自動的に終了する時間:" @@ -749,6 +777,7 @@ ja: select_file: "ファイル選択" image_link: "イメージのリンク先" search: + select_all: "全てを選択する" title: "トピック、ポスト、ユーザ、カテゴリを探す" no_results: "何も見つかりませんでした。" no_more_results: "これ以上結果が見つかりませんでした。" @@ -1723,6 +1752,10 @@ ja: last_seen_user: "ユーザが最後にサイトを訪れた日:" reply_key: "回答キー" skipped_reason: "スキップの理由" + incoming_emails: + error: "エラー" + filters: + error_placeholder: "エラー" logs: none: "ログなし" filters: @@ -1741,6 +1774,7 @@ ja: ip_address: "IP" topic_id: "トピックID" post_id: "ポストID" + category_id: "カテゴリID" delete: '削除' edit: '編集' save: '保存' @@ -1782,6 +1816,12 @@ ja: anonymize_user: "匿名ユーザ" roll_up: "IPブロックをロールアップ" delete_category: "カテゴリを削除する" + block_user: "ユーザをブロック" + unblock_user: "ユーザをブロック解除" + grant_admin: "管理者権限を付与" + revoke_admin: "管理者権限を剥奪" + grant_moderation: "モデレータ権限を付与" + revoke_moderation: "モデレータ権限を剥奪" screened_emails: title: "ブロック対象アドレス" description: "新規アカウント作成時、次のメールアドレスからの登録をブロックする。" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 03b14e28ec..b68af57710 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -81,6 +81,8 @@ ko: other: "%{count}달 후" x_years: other: "%{count}년 후" + previous_month: '이전 달' + next_month: '다음 달' share: topic: '토픽을 공유합니다.' post: '게시글 #%{postNumber}' @@ -118,6 +120,7 @@ ko: us_east_1: "미국 동부 (N. 버지니아)" us_west_1: "미국 서부 (N. 캘리포니아)" us_west_2: "미국 서부 (오레곤)" + us_gov_west_1: "AWS GovCloud(US)" eu_west_1: "유럽연합 (아일랜드)" eu_central_1: "유럽연합 (프랑크푸르트)" ap_southeast_1: "아시아 태평양 (싱가폴)" @@ -166,6 +169,7 @@ ko: other: "{{count}} 자" suggested_topics: title: "추천 토픽" + pm_title: "추천 메세지" about: simple_title: "About" title: "About %{title}" @@ -391,12 +395,13 @@ ko: invited_by: "(이)가 초대했습니다." trust_level: "신뢰도" notifications: "알림" + statistics: "통계" desktop_notifications: label: "데스크탑 알림" not_supported: "안타깝게도 지금 사용하고 계시는 브라우저는 알림을 지원하지 않습니다." perm_default: "알림 켜기" perm_denied_btn: "권한 거부" - perm_denied_expl: "알림에 대한 권한이 없습니다. 알림을 활성화하기위해 브라우저의 알림설정을 하시고 난 후에 버튼을 눌러주세요. (데스크탑: 주소 표시 막대 가장 왼쪽에 있는 아이콘, 모바일: '사이트 정보'.)" + perm_denied_expl: "통지를 위한 허용을 거절했었습니다. 브라우저 설정을 통해서 통지를 허용해주세요." disable: "알림 비활성화" currently_enabled: "(현재 활성화됨)" enable: "알림 활성화" @@ -450,6 +455,7 @@ ko: groups: "내 그룹" bulk_select: "메시지 선택" move_to_inbox: "수신함으로 이동" + move_to_archive: "보관하기" failed_to_move: "선택한 메시지를 이동할 수 없습니다 (아마도 네트워크가 다운됨)" select_all: "모두 선택" change_password: @@ -574,6 +580,7 @@ ko: other: "앞 {{count}}개의 초대를 보여줍니다." redeemed: "초대를 받았습니다." redeemed_tab: "Redeemed" + redeemed_tab_with_count: "교환된 ({{count}})" redeemed_at: "에 초대되었습니다." pending: "초대를 보류합니다." pending_tab: "보류" @@ -607,6 +614,7 @@ ko: instructions: "글자 수가 %{count}자 이상이어야 합니다." summary: title: "요약" + stats: "통계" topic_count: "생성된 토픽" post_count: "생성된 포스트" likes_given: "좋아요 누름" @@ -617,6 +625,8 @@ ko: top_topics: "최고 인기 토픽" top_badges: "최고 뱃지" more_topics: "다른이에게 준 좋아요" + more_replies: "더 많은 답글" + more_badges: "뱃지 더 보기" associated_accounts: "로그인" ip_address: title: "마지막 IP 주소" @@ -648,6 +658,7 @@ ko: network_fixed: "문제가 해결된 것으로 보입니다." server: "에러 코드: {{status}}" forbidden: "볼 수 있도록 허용되지 않았습니다." + not_found: "에구, 어플리케이션이 없는 URL를 가져오려고 시도했습니다." unknown: "문제가 발생했습니다." buttons: back: "뒤로가기" @@ -741,6 +752,7 @@ ko: sent_activation_email_again: " {{currentEmail}} 주소로 인증 이메일을 보냈습니다. 이메일이 도착하기까지 몇 분 정도 걸릴 수 있습니다. 또한 스팸 메일을 확인하십시오." to_continue: "로그인 해주세요" preferences: "사용자 환경을 변경하려면 로그인이 필요합니다." + forgot: "내 계정의 상세내역 기억하지 않는다." google: title: "Google" message: "Google 인증 중(팝업 차단을 해제 하세요)" @@ -768,9 +780,12 @@ ko: ctrl: 'Ctrl' alt: 'Alt' composer: + emoji: "이모지 :)" more_emoji: "더보기..." options: "온셥" + whisper: "귓속말" add_warning: "공식적인 경고입니다." + toggle_whisper: "귀속말 켜고 끄기" posting_not_on_topic: "어떤 토픽에 답글을 작성하시겠습니까?" saving_draft_tip: "저장 중..." saved_draft_tip: "저장 완료" @@ -798,6 +813,7 @@ ko: edit_reason_placeholder: "why are you editing?" show_edit_reason: "(add edit reason)" view_new_post: "새로운 글을 볼 수 있습니다." + saving: "저장 중..." saved: "저장 완료!" saved_draft: "작성중인 글이 있습니다. 계속 작성하려면 여기를 클릭하세요." uploading: "업로딩 중..." @@ -812,6 +828,7 @@ ko: link_description: "링크 설명을 입력" link_dialog_title: "하이퍼링크 삽입" link_optional_text: "옵션 제목" + link_placeholder: "http://example.com \"선택적 텍스트\"" quote_title: "인용구" quote_text: "인용구" code_title: "코드 샘플" @@ -828,6 +845,7 @@ ko: toggler: "작성 패널을 숨기거나 표시" modal_ok: "OK" modal_cancel: "취소" + cant_send_pm: "죄송합니다. %{username}님에게 메시지를 보낼 수 없습니다." admin_options_title: "이 토픽에 대한 옵션 설정" auto_close: label: "토픽 자동-닫기 시간:" @@ -859,7 +877,18 @@ ko: alt: mentioned: "멘션 by" quoted: "인용 by" + replied: "답글을 전송했습니다." posted: "포스트 by" + edited: "당신 글이 다음 이용자에 의해 수정" + liked: "당신의 글을 좋아했음." + private_message: "다음 사람에게서온 개인 메시지" + invited_to_private_message: "다음 사람으로부터 개인 메시지가 초대됨" + invited_to_topic: "다음 사람으로부터 한 주제로 초대됨" + invitee_accepted: "다음 사람에 의해 초대가 수락됨." + moved_post: "다음 사람에 의해서 당신의 글이 이동됨" + linked: "당신 글로 링크하기" + granted_badge: "뱃지가 수여됨." + group_message_summary: "그룹 메시지함의 메시지" popup: mentioned: '"{{topic}}" - {{site_title}}에서 {{username}} 님이 나를 멘션했습니다' quoted: '"{{topic}}" - {{site_title}}에서 {{username}} 님이 나를 인용했습니다' @@ -879,7 +908,12 @@ ko: select_file: "파일 선택" image_link: "이 이미지를 누르면 이동할 링크" search: + sort_by: "다음으로 정렬" + latest_post: "가장 최근 글" + most_viewed: "가장 많이 본" + most_liked: "가장 많이 좋아요를 받은" select_all: "모두 선택" + clear_all: "다 지우기" title: "토픽, 글, 사용자, 카테고리 검색" no_results: "검색 결과가 없습니다" no_more_results: "더 이상 결과가 없습니다." @@ -892,6 +926,7 @@ ko: topic: "이 토픽을 검색" private_messages: "메시지 검색" hamburger_menu: "다른 토픽 목록이나 카테고리로 가기" + new_item: "새로운" go_back: '돌아가기' not_logged_in_user: 'user page with summary of current activity and preferences' current_user: '사용자 페이지로 이동' @@ -899,6 +934,9 @@ ko: bulk: reset_read: "읽기 초기화" delete: "토픽 삭제" + dismiss: "해지" + dismiss_read: "읽지않음 전부 해지" + dismiss_button: "해지..." dismiss_new: "새글 제거" toggle: "토픽 복수 선택" actions: "일괄 적용" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 215061353a..84dbeab8a2 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -100,6 +100,8 @@ pt_BR: x_years: one: "1 ano depois" other: "%{count} anos depois" + previous_month: 'Mês Anterior' + next_month: 'Próximo Mês' share: topic: 'compartilhe o link desse tópico' post: 'post #%{postNumber}' @@ -110,6 +112,7 @@ pt_BR: email: 'enviar esse link para um email' action_codes: split_topic: "dividiu este tópico %{when}" + invited_user: "convidou %{who} %{when}" autoclosed: enabled: 'fechou %{when}' disabled: 'abriu %{when}' @@ -130,6 +133,9 @@ pt_BR: disabled: 'desalistou %{when}' topic_admin_menu: "ações administrativas do tópico" emails_are_disabled: "Todo o envio de email foi globalmente desabilitado por algum administrador. Nenhum email de notificações de qualquer tipo será enviado." + s3: + regions: + sa_east_1: "América do Sul (São Paulo)" edit: 'edite o título e a categoria deste tópico' not_implemented: "Esse recurso ainda não foi implementado, desculpe!" no_value: "Não" @@ -173,6 +179,7 @@ pt_BR: other: "{{count}} caracteres" suggested_topics: title: "Tópicos sugeridos" + pm_title: "Mensagens Sugeridas" about: simple_title: "Sobre" title: "Sobre %{title}" @@ -294,6 +301,12 @@ pt_BR: one: "1 usuário" other: "%{count} usuários" groups: + empty: + posts: "Não há postagens por membros deste grupo." + members: "Não há membros neste grupo." + mentions: "Não há menção a este grupo." + messages: "Não há mensagens para este grupo." + topics: "Não há topicos por membros deste grupo." add: "Adicionar" selector_placeholder: "Adicionar membros" owner: "proprietário" @@ -304,6 +317,7 @@ pt_BR: members: "Membros" posts: "Mensagens" alias_levels: + title: "Quem pode enviar mensagem e @mention a este grupo?" nobody: "Ninguém" only_admins: "Somente administradores" mods_and_admins: "Somente moderadores e Administradores" @@ -312,6 +326,18 @@ pt_BR: trust_levels: title: "Nível de Confiança automaticamente concedido aos membros quando eles são incluídos" none: "Nenhum" + notifications: + watching: + description: "Você será notificado sobre toda nova postagem em toda mensagem, e uma contagem de novas mensagens será mostrada." + tracking: + title: "Rastreando" + description: "Você será notificado se alguém mencionar seu @name ou responder a você, e uma contagem de novas respostas será mostrada." + regular: + title: "Normal" + description: "Você será notificado se alguém mencionar seu @name ou responder a você." + muted: + title: "Mudo" + description: "Você será notificado sobre qualquer coisa sobre novos tópicos neste grupo." user_action_groups: '1': "Curtidas dadas" '2': "Curtidas recebidas" @@ -330,6 +356,7 @@ pt_BR: all_subcategories: "todos" no_subcategory: "nenhum" category: "Categoria" + category_list: "Exibir lista de categorias." reorder: title: "Reordenar Categorias" title_long: "Reorganizar a lista de categorias" diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 7e4c3935dc..d0ddfd41d7 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -8,24 +8,32 @@ ro: js: number: + format: + separator: "." + delimiter: "," human: storage_units: format: '%n %u' units: byte: - one: Bit - few: Biți - other: Biți + one: Byte + few: Byte + other: Byte gb: GB kb: KB mb: MB tb: TB + short: + thousands: "{{number}}k" + millions: "{{number}}M" dates: time: "HH:mm" long_no_year: "DD MMM HH:mm" long_no_year_no_time: "DD MMM" + full_no_year_no_time: "Do MMMM " long_with_year: "DD MMM YYYY HH:mm" long_with_year_no_time: "DD MMM YYYY" + full_with_year_no_time: "Do MMMM YYYY" long_date_with_year: "DD MMM 'YY HH:mm" long_date_without_year: "DD MMM HH:mm" long_date_with_year_without_time: "DD MMM 'YY" @@ -69,8 +77,8 @@ ro: one: "1a" few: "%{count}a" other: "%{count}a" - date_month: "Z LLL" - date_year: "LLL 'AA" + date_month: "DD MMMM" + date_year: "MMM 'YY" medium: x_minutes: one: "1 min" @@ -84,10 +92,10 @@ ro: one: "1 zi" few: "%{count} zile" other: "%{count} zile" - date_year: "Z LL AAAA" + date_year: "D MM YYYY" medium_with_ago: x_minutes: - one: "acum 1 min" + one: "acum un min" few: "acum %{count} min" other: "acum %{count} min" x_hours: @@ -100,28 +108,34 @@ ro: other: "acum %{count} zile" later: x_days: - one: "După 1 zi" + one: "După o zi" few: "După %{count} zile" other: "După %{count} zile" + x_months: + one: "o lună mai târziu" + few: "%{count} luni mai târziu" + other: "%{count} de luni mai târziu" x_years: - one: "După 1 an" + one: "După un an" few: "După %{count} ani" other: "După %{count} ani" + previous_month: 'Luna anterioară' + next_month: 'Luna următoare' share: - topic: 'distribuie adresă către această discuție' - post: 'distribuie o adresă către postarea #%{postNumber}' + topic: 'distribuie această discuție' + post: 'distribuie postarea #%{postNumber}' close: 'închide' - twitter: 'distribuie această adresă pe Twitter' - facebook: 'distribuie această adresă pe Facebook' - google+: 'distribuie această adresă pe Google+' - email: 'trimite această adresă în email' + twitter: 'distribuie pe Twitter' + facebook: 'distribuie pe Facebook' + google+: 'distribuie pe Google+' + email: 'trimite această adresă peemail' action_codes: - split_topic: "despartiti acest topic %{when}" + split_topic: "desparțiți acest topic %{when}" autoclosed: - enabled: 'inchis %{count}' + enabled: 'închis %{count}' disabled: 'deschis %{when}' closed: - enabled: 'inchis %{when}' + enabled: 'închis %{when}' disabled: 'deschis %{when}' archived: enabled: 'arhivat %{when}' @@ -137,6 +151,19 @@ ro: disabled: 'retras %{when}' topic_admin_menu: "acțiuni subiect administrator" emails_are_disabled: "Trimiterea de emailuri a fost dezactivată global de către un administrator. Nu vor fi trimise notificări email de nici un fel." + s3: + regions: + us_east_1: "US East (N. Virginia)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" + us_gov_west_1: "AWS GovCloud (US)" + eu_west_1: "EU (Irlanda)" + eu_central_1: "EU (Frankfurt)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" + ap_northeast_1: "Asia Pacific (Tokyo)" + ap_northeast_2: "Asia Pacific (Seoul)" + sa_east_1: "South America (Sao Paulo)" edit: 'editează titlul și categoria acestui subiect' not_implemented: "Această caracteristică nu a fost implementată încă, ne pare rău!" no_value: "Nu" @@ -150,7 +177,7 @@ ro: admin_title: "Admin" flags_title: "Semnalare" show_more: "Detaliază" - show_help: "Optiuni" + show_help: "Opțiuni" links: "Adrese" links_lowercase: one: "adresă" @@ -182,6 +209,7 @@ ro: other: "{{count}} caractere" suggested_topics: title: "Subiecte Propuse" + pm_title: "Subiecte Propuse" about: simple_title: "Despre" title: "Despre %{title}" @@ -228,7 +256,7 @@ ro: preview: "vizualizează" cancel: "anulează" save: "Salvează Schimbările" - saving: "Salvează..." + saving: "Se Salvează..." saved: "Salvat!" upload: "Încarcă" uploading: "Încărcare..." @@ -310,6 +338,8 @@ ro: other: "%{count} utilizatori" groups: add: "Adăugați" + selector_placeholder: "Adaugă membri" + owner: "proprietar" visible: "Grupul este vizibil tuturor utilizatorilor" title: one: "grup" @@ -323,6 +353,9 @@ ro: mods_and_admins: "Doar moderatorii și adminii" members_mods_and_admins: "Doar membri grupului, moderatorii și adminii" everyone: "Toată lumea" + notifications: + regular: + title: "Normal" user_action_groups: '1': "Aprecieri Date" '2': "Aprecieri Primite" @@ -447,6 +480,9 @@ ro: warnings_received: "avertizări" messages: all: "Toate" + sent: "Trimise" + archive: "Arhivează" + move_to_archive: "Arhivează" change_password: success: "(email trimis)" in_progress: "(se trimite email)" @@ -592,6 +628,9 @@ ro: same_as_email: "Parolă este identică cu adresa de email" ok: "Parola dumneavoastră arată bine." instructions: "Trebuiesc minim %{count} de caractere." + summary: + likes_given: "Aprecieri Date" + likes_received: "Aprecieri Primite" associated_accounts: "Conectări" ip_address: title: "Ultima adresă de IP" @@ -915,6 +954,8 @@ ro: create: 'Crează discuție' create_long: 'Crează discuție nouă' private_message: 'Scrie un mesaj.' + archive_message: + title: 'Arhivează' list: 'Discuții' new: 'discuție nouă' unread: 'necitită' @@ -1002,6 +1043,10 @@ ro: title: "Urmărind" tracking: title: "Urmărind" + regular: + title: "Normal" + regular_pm: + title: "Normal" muted_pm: title: "Silențios" description: "Nu veţi fi niciodată notificat despre acest mesaj." @@ -1324,6 +1369,7 @@ ro: category: can: 'can… ' none: '(nicio categorie)' + all: 'Toate categoriile' choose: 'Selectează o categorie…' edit: 'editează' edit_long: "Editează" @@ -1375,6 +1421,8 @@ ro: title: "Vizualizare" tracking: title: "Urmărire" + regular: + title: "Normal" muted: title: "Silențios" flagging: @@ -1391,6 +1439,7 @@ ro: submit_tooltip: "Acceptă marcarea privată" take_action_tooltip: "Accesati permisiunea marcarii imediat, nu mai asteptati alte marcaje comune" cant: "Ne pare rău nu puteți marca această postare deocamdată." + notify_staff: 'Anunță moderatorii' formatted_name: off_topic: "În afară discuției" inappropriate: "Inadecvat" @@ -1467,6 +1516,7 @@ ro: with_topics: "%{filter} Discuții" with_category: "%{filter} %{category} discuții" latest: + title: "Ultimele" help: "Discuții cu postări recente" hot: title: "Interesant" @@ -1482,9 +1532,15 @@ ro: title_in: "Categoria - {{categoryName}}" help: "toate discuțiile grupate pe categorii" unread: + title: "Necitite" + title_with_count: + one: "Necitit (1)" + few: "Necitite ({{count}})" + other: "Necitite ({{count}})" help: "discuțiile pe care le vizualizați sau urmariți momentan ce includ postări necitite" new: lower_title: "noi" + title: "Nou" help: "discuții create în ultimele zile" posted: title: "Postările mele" @@ -1501,6 +1557,8 @@ ro: title: "Dintotdeauna" yearly: title: "Anual" + quarterly: + title: "Trimestrial" monthly: title: "Lunar" weekly: @@ -1508,7 +1566,12 @@ ro: daily: title: "Zilnic" all_time: "Dintotdeauna" + this_year: "An" + this_quarter: "Trimestru" + this_month: "Lună" + this_week: "Săptămană" today: "Astăzi" + other_periods: "vezi topul" browser_update: 'Din nefericire, browserul dumneavoastră este prea vechi pentru a funcționa pe acest forum . Va rugăm reânoiți browserul.' permission_types: full: "Crează / Răspunde / Vizualizează" @@ -1542,9 +1605,10 @@ ro: suspended: 'Suspendați:' private_messages_short: "Msgs" private_messages_title: "Mesaje" + mobile_title: "Mobil" space_free: "{{size}} liber" uploads: "încărcări" - backups: "salvări" + backups: "backups" traffic_short: "trafic" traffic: "Cereri web" page_views: "Cereri API" @@ -1564,6 +1628,7 @@ ro: refresh_report: "Reactualizează Raportul" start_date: "Data de început " end_date: "Data de sfârşit" + groups: "Toate grupurile" commits: latest_changes: "Ultimele schimbări: Vă rugăm reactualizați des!" by: "de către" @@ -1649,15 +1714,23 @@ ro: delete_confirm: "Șterg acest grup?" delete_failed: "Imposibil de șters grupul. Dacă este unul automat, nu se poate șterge." delete_member_confirm: "Şterge '%{username}' din grupul '%{group}'?" + delete_owner_confirm: "Revocă dreptul de proprietar pentru '%{username}'?" name: "Nume" add: "Adaugă" add_members: "Adaugă membri" custom: "Personalizat" + bulk_complete: "Utilizatorii au fost adăugați în grup." + bulk: "Adaugă în grup la grămadă" + bulk_paste: "Lipiți o listă de utilizatori sau email-uri, unul pe linie:" + bulk_select: "(selectați un grup)" automatic: "Automat" automatic_membership_email_domains: "Utilizatorii care se înregistrează cu un domeniu de email care se potriveşte cu unul din lista va fi adăugat automat în aces grup:" automatic_membership_retroactive: "Aplicaţi aceeaşi regulă pentru domeniul de email pentru a adaugă utilizatorii existenţi" default_title: "Titlu automat pentru toţi utilizatorii din acest grup" primary_group: "Setează automat că grup primar" + group_owners: Proprietari + add_owners: Adaugă proprietari + incoming_email_placeholder: "introduceți adresa de email" api: generate_master: "Generează cheie API principală" none: "Nu sunt chei API principale active deocamdată." @@ -1678,12 +1751,16 @@ ro: name: "Nume" none_installed: "Nu aveţi nici un plugin instalat." version: "Versiune" + enabled: "Activat?" + is_enabled: "D" + not_enabled: "N" change_settings: "Schimbă Setările" + change_settings_short: "Setări" howto: "Cum instalez un plugin?" backups: - title: "Rezervare" + title: "Backups" menu: - backups: "Rezerve" + backups: "Backups" logs: "Rapoarte" none: "Nicio rezervare valabilă." read_only: @@ -1711,7 +1788,7 @@ ro: cancel: label: "Anulează" title: "Anulează operația curentă" - confirm: "Sunteți sigur că doriți să anulati operația curentă?" + confirm: "Sunteți sigur că doriți să anulați operația curentă?" backup: label: "Salvare de siguranţă" title: "Creați o rezervă" @@ -1744,6 +1821,8 @@ ro: screened_email: "Exportă lista totală a adreselor de email verificate în format CSV." screened_ip: "Exportă lista totală a adreselor de IP verificate în format CSV." screened_url: "Exportă lista totală a adreselor URL verificate în format CSV." + export_json: + button_text: "Exportă" invite: button_text: "Trimite o invitație" button_title: "Trimite o invitație" @@ -1754,6 +1833,7 @@ ro: header: "Titlu" top: "Top" footer: "Subsol" + embedded_css: "Embedded CSS" head_tag: text: "" title: "HTML care va fi inserat înaintea de tag-ul " @@ -1771,12 +1851,22 @@ ro: save: "Salvează" new: "Nou" new_style: "Stil nou" + import: "Importă" + import_title: "Selectați un fișier sau lipiți un text" delete: "Șterge" delete_confirm: "Șterge aceste preferințe?" about: "Modifică foaia de stil CSS și capetele HTML Modify CSS din site. Adaugă o preferința pentru a începe." color: "Culoare" opacity: "Opacitate" copy: "Copiază" + email_templates: + title: "Sabloane" + subject: "Subiect" + multiple_subjects: "Acest șablon are mai multe subiecte" + body: "Body" + none_selected: "Selectați un șablon pentru a începe editarea" + revert: "Revocați schimbările" + revert_confirm: "Ești sigur că vreți să revocați schimbările?" css_html: title: "CSS/HTML" long_title: "Customizarile CSS and HTML" @@ -1825,13 +1915,17 @@ ro: name: 'wiki' description: "Culoarea de bază folosită pentru fundalul postărilor pe wiki." email: + title: "Emails" settings: "Opțiuni" + templates: "Șabloane" preview_digest: "Previzualizează rezumat" sending_test: "Trimite email de test..." error: "EROARE - %{server_error}" test_error: "S-a semnalat o problemă la trimtirerea email-ului. Vă rugăm verificați setările mailului, Verificați ca gazda sa nu bocheze conexiunile de email și reâncercați." sent: "Trimise" skipped: "Omise" + received: "Primite" + rejected: "Respinse" sent_at: "Trimise la" time: "Timp" user: "Utilizator" @@ -1848,6 +1942,18 @@ ro: last_seen_user: "Ultimul utilizator văzut:" reply_key: "Cheie de răspuns" skipped_reason: "Motiv omiterii" + incoming_emails: + from_address: "De la" + to_addresses: "Către" + cc_addresses: "Cc" + subject: "Subiect" + error: "Eroare" + filters: + from_placeholder: "from@example.com" + to_placeholder: "to@example.com" + cc_placeholder: "cc@example.com" + subject_placeholder: "Subiect..." + error_placeholder: "Eroare" logs: none: "Nu s-au găsit rapoarte." filters: @@ -1897,6 +2003,7 @@ ro: change_site_setting: "schimbă setările site-ului" change_site_customization: "schimbă preferințele site-ului" delete_site_customization: "șterge preferințele site-ului" + change_site_text: "schimbă textul site-ului" suspend_user: "suspendă utilizator" unsuspend_user: "reactivează utilizator" grant_badge: "acordă insignă" @@ -1906,6 +2013,15 @@ ro: delete_post: "şterge mesajul" impersonate: "joacă rolul" anonymize_user: "fă userul anonim" + change_category_settings: "schimbă setările categoriei" + delete_category: "șterge categorie" + create_category: "crează categorie" + block_user: "blochează utilizator" + unblock_user: "deblochează utilizator" + grant_admin: "Acordă titlul de Admin" + revoke_admin: "Revocă titlul de Admin" + grant_moderation: "Acordă titlul de Moderator" + revoke_moderation: "Revocă titlul de Moderator" screened_emails: title: "Email-uri filtrate" description: "Când cineva încearcă să creeze un nou cont, următorul email va fi verificat iar înregistrarea va fi blocată, sau o altă acțiune va fi inițiată." @@ -1970,6 +2086,9 @@ ro: pending: 'Utilizatori în așteptare de previzualizare' newuser: 'Utilizatori la nielul de încredere 0 (utilizator nou)' basic: 'Utilizatori la nivel de încredere 1 (utilizator de baza)' + member: 'Utilizatori la nivel de încredere 2 (Membri)' + regular: 'Utilizatori la nivel de încredere 3 (Utilizator activ)' + leader: 'Utilizatori la nivel de încredere 4 (Lider)' staff: "Personalul" admins: 'Utilizatori admin' moderators: 'Moderatori' @@ -2071,6 +2190,7 @@ ro: deactivate_failed: "S-a semnalat o problemă la dezactivarea utilizatoprului." unblock_failed: 'S-a semnalat o problemă la deblocarea utlizatorului.' block_failed: 'S-a semnalat o problemă la blocarea utilizatorului.' + block_accept: 'Blochează utilizatorul' deactivate_explanation: "Un utilizator dezactivat va trebuii sa-și reactvieze emailul." suspended_explanation: "Un utilizator suspendat nu se poate autentifica" block_explanation: "Un utilizator blocat nu poate posta sau pornii o discuție." @@ -2126,6 +2246,7 @@ ro: delete: "Șterge" cancel: "Anulează" delete_confirm: "Sunteți sigur că stergeți acest câmp utilizator?" + options: "Optiuni" required: title: "Necesar la înscriere?" enabled: "necesar" @@ -2141,8 +2262,14 @@ ro: field_types: text: 'Câmp Text' confirm: 'Confirmare' + dropdown: "Select" site_text: title: 'Conținut' + edit: 'editează' + revert: "Revocați schimbările" + revert_confirm: "Ești sigur că vreți să revocați schimbările?" + go_back: "Înapoi la căutare" + show_overriden: 'Arată doar rescrierile' site_settings: show_overriden: 'Arată doar rescrierile' title: 'Setări' @@ -2172,6 +2299,7 @@ ro: backups: "Rezervări" login: "Autentificare" plugins: "Plugin-uri" + user_preferences: "Preferințe" badges: title: Insigne new_badge: Insignă nouă @@ -2241,6 +2369,16 @@ ro: image: "Imagine" delete_confirm: "Sunteţi sigur că doriţi să ștergeți :%{name}: emoji?" embedding: + confirm_delete: "Sunteți sigur că vreți să ștergeți acest host?" + sample: "Folosiți următorul cod HTML în site-ul dvs. pentru a crea și pentru a embed-ui topic-uri discourse. Înlocuiți REPLACE_ME cu URL-ul canonic al paginii pe care doriți să o embed-uiți." + title: "Embedding" + host: "Host-uri permise" + edit: "editează" + category: "Postează în categoria" + add_host: "Adaugă host" + settings: "Setări pentru embeding" + feed_settings: "Setări Feed" + embed_post_limit: "Numărul maxim de postări de încorporat." save: "Salvați setările pentru embeding" permalink: title: "Link-uri" @@ -2312,6 +2450,7 @@ ro: mark_tracking: 'm, t Urmăriți discuția' mark_watching: 'm, w Urmăriți discuția îndeaproape' badges: + more_with_badge: "Alții cu această insignă" title: Insigne allow_title: "poate fi folosit ca titlu" multiple_grant: "pot fi acordate de mai multe ori" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index a3da018129..7623c339b5 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -81,6 +81,8 @@ vi: other: "còn %{count} tháng" x_years: other: "còn %{count} năm" + previous_month: 'Tháng Trước' + next_month: 'Tháng Sau' share: topic: 'chia sẽ chủ đề này' post: 'đăng #%{Bài đăng số}' @@ -91,6 +93,8 @@ vi: email: 'Gửi liên kết này qua thư điện tử' action_codes: split_topic: "chìa chủ đề này lúc %{when}" + invited_user: "đã mời bởi %{who} %{when}" + removed_user: "loại bỏ bởi %{who} %{when}" autoclosed: enabled: 'đóng lúc %{when}' disabled: 'mở lúc %{when}' @@ -111,6 +115,19 @@ vi: disabled: 'bỏ lưu %{when}' topic_admin_menu: "quản lí chủ đề." emails_are_disabled: "Ban quản trị đã chặn mọi email đang gửi. Sẽ không có bắt kỳ thông báo nào về email được gửi đi." + s3: + regions: + us_east_1: "US East (N. Virginia)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" + us_gov_west_1: "AWS GovCloud (US)" + eu_west_1: "EU (Ireland)" + eu_central_1: "EU (Frankfurt)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" + ap_northeast_1: "Asia Pacific (Tokyo)" + ap_northeast_2: "Asia Pacific (Seoul)" + sa_east_1: "South America (Sao Paulo)" edit: 'thay đổi tiêu đề và chuyên mục của chủ đề' not_implemented: "Tính năng này chưa được hoàn thiện hết, xin lỗi!" no_value: "Không" @@ -152,6 +169,7 @@ vi: other: "{{count}} ký tự" suggested_topics: title: "Chủ đề tương tự" + pm_title: "Tin nhắn gợi ý" about: simple_title: "Giới thiệu" title: "Giới thiệu về %{title}" @@ -267,12 +285,22 @@ vi: total_rows: other: "%{count} người dùng" groups: + empty: + posts: "Không có chủ đề của các thành viên trong nhóm" + members: "Không có thành viên nào trong nhóm" + mentions: "Không có thành viên nào trong nhóm" + messages: "Không có tin nhắn nào trong nhóm" + topics: "Không có chủ đề của các thành viên trong nhóm" + add: "Thêm" + selector_placeholder: "Thêm thành viên" + owner: "chủ" visible: "Mọi thành viên có thể nhìn thấy nhóm" title: other: "các nhóm" members: "Các thành viên" posts: "Các bài viết" alias_levels: + title: "Ai có thể nhắn tin và @mention trong nhóm này?" nobody: "Không ai cả" only_admins: "Chỉ các quản trị viên" mods_and_admins: "Chỉ có người điều hành và ban quản trị" @@ -281,6 +309,19 @@ vi: trust_levels: title: "Cấp độ tin tưởng tự động tăng cho thành viên khi họ thêm:" none: "Không có gì" + notifications: + watching: + title: "Đang xem" + description: "Bạn sẽ được thông báo khi có bài viết mới trong mỗi tin nhắn, và số lượng trả lời mới sẽ được hiển thị" + tracking: + title: "Đang theo dõi" + description: "Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn, và số lượng trả lời mới sẽ được hiển thị" + regular: + title: "Bình thường" + description: "Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn" + muted: + title: "Im lặng" + description: "Bạn sẽ không bao giờ được thông báo về bất cứ chủ đề mới nào trong nhóm này" user_action_groups: '1': "Lần thích" '2': "Lần được thích" @@ -299,6 +340,7 @@ vi: all_subcategories: "Tất cả" no_subcategory: "không có gì" category: "Chuyên mục" + category_list: "Hiễn thị danh sách chuyên mục" reorder: title: "Sắp xếp lại danh mục" title_long: "Tổ chức lại danh sách danh mục" @@ -353,12 +395,13 @@ vi: invited_by: "Được mời bởi" trust_level: "Độ tin tưởng" notifications: "Thông báo" + statistics: "Thống kê" desktop_notifications: label: "Desktop Notifications" not_supported: "Xin lỗi. Trình duyệt của bạn không hỗ trợ Notification." perm_default: "Mở thông báo" perm_denied_btn: "Không có quyền" - perm_denied_expl: "Bạn bị từ chối quyền cho notification. Dùng trình duyệt của bạn để kích hoạt notification, sau đó nhấp nút này khi hoàn thành. (Desktop: Icon bên trái của thanh địa chỉ. Mobile: 'Site Info'.)" + perm_denied_expl: "Bạn đã từ chối nhận thông báo, để nhận lại bạn cần thiết lập trình duyệt." disable: "Khóa Notification" currently_enabled: "(đang cho phép)" enable: "Cho phép Notification" @@ -386,6 +429,7 @@ vi: tracked_categories: "Theo dõi" tracked_categories_instructions: "Bạn sẽ tự động theo dõi tất cả các chủ đề trong các chuyên mục này. Một số bài viết mới sẽ xuất hiện ở chủ đề kế tiếp." muted_categories: "Im lặng" + muted_categories_instructions: "Bạn sẽ không bao giờ được thông báo về bất cứ điều gì về các chủ đề mới trong các chuyên mục này, và chúng sẽ không hiển thị mới nhất" delete_account: "Xoá Tài khoản của tôi" delete_account_confirm: "Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của bạn? Hành động này không thể được hoàn tác!" deleted_yourself: "Tài khoản của bạn đã được xóa thành công." @@ -395,6 +439,8 @@ vi: users: "Thành viên" muted_users: "Im lặng" muted_users_instructions: "Ngăn chặn tất cả các thông báo từ những thành viên." + muted_topics_link: "Hiển thị chủ đề Im Lặng" + automatically_unpin_topics: "Tự động theo dõi tiêu đề bạn vào " staff_counters: flags_given: "cờ hữu ích" flagged_posts: "bài viết gắn cờ" @@ -403,6 +449,15 @@ vi: warnings_received: "cảnh báo" messages: all: "Tất cả" + inbox: "Hộp thư" + sent: "Đã gửi" + archive: "Lưu Trữ" + groups: "Nhóm của tôi" + bulk_select: "Chọn tin nhắn" + move_to_inbox: "Chuyển sang hộp thư" + move_to_archive: "Lưu trữ" + failed_to_move: "Lỗi khi chuyển các tin nhắn đã chọn (có thể do lỗi mạng)" + select_all: "Chọn tất cả" change_password: success: "(email đã gửi)" in_progress: "(đang gửi email)" @@ -447,6 +502,9 @@ vi: ok: "Chúng tôi sẽ gửi thư điện tử xác nhận đến cho bạn" invalid: "Vùi lòng nhập một thư điện tử hợp lệ" authenticated: "Thư điện tử của bạn đã được xác nhận bởi {{provider}}" + frequency_immediately: "Chúng tôi sẽ gửi email cho bạn ngay lập tức nếu bạn đã chưa đọc những điều chúng tôi đã gửi cho bạn qua email." + frequency: + other: "Chúng tôi sẽ chỉ gửi email cho bạn nếu chúng tôi đã không nhìn thấy bạn trong {{count}} phút cuối." name: title: "Tên" instructions: "Tên đầy đủ của bạn (tùy chọn)" @@ -517,6 +575,9 @@ vi: title: "Lời mời" user: "User được mời" sent: "Đã gửi" + none: "Không có thư mời nào đang chờ để hiển thị" + truncated: + other: "Hiện {{count}} thư mời đầu tiên" redeemed: "Lời mời bù lại" redeemed_tab: "Làm lại" redeemed_tab_with_count: "Làm lại ({{count}})" @@ -536,6 +597,7 @@ vi: account_age_days: "Thời gian của tài khoản theo ngày" create: "Gửi một lời mời" generate_link: "Chép liên kết Mời" + generated_link_message: '

      Liên kết thư mời được tạo thành công!

      Liên kết thư mời chỉ hợp lệ cho email này: %{invitedEmail}

      ' bulk_invite: none: "Bạn đã mời ai ở đây chưa. Bạn có thể mời một hoặc một nhóm bằng tải lên hàng loạt file mời." text: "Mời hàng loạt bằng file" @@ -550,6 +612,21 @@ vi: same_as_email: "Mật khẩu của bạn trùng với email của bạn." ok: "Mật khẩu của bạn có vẻ ổn." instructions: "Ít nhất %{count} ký tự" + summary: + title: "Tóm tắt" + stats: "Thống kê" + topic_count: "Chủ đề đã tạo" + post_count: "Bài viết đã tạo" + likes_given: "Lượt Likes" + likes_received: "Likes đã nhận" + days_visited: "Ngày đã ghé thăm" + posts_read_count: "Bài viết đã đọc" + top_replies: "Top trả lời" + top_topics: "Top chủ đề" + top_badges: "Top huy hiệu" + more_topics: "Thêm chủ đề" + more_replies: "Thêm trả lời" + more_badges: "Thêm huy hiệu" associated_accounts: "Đăng nhập" ip_address: title: "Địa chỉ IP cuối cùng" @@ -581,6 +658,7 @@ vi: network_fixed: "Hình như nó trở lại." server: "Mã lỗi : {{status}}" forbidden: "Bạn không được cho phép để xem mục này" + not_found: "Oops, ứng dụng đang tải đường dẫn không tồn tại" unknown: "Có một lỗi gì đó đang xảy ra" buttons: back: "Quay trở lại" @@ -591,8 +669,11 @@ vi: logout: "Bạn đã đăng xuất" refresh: "Tải lại" read_only_mode: - enabled: "Chế độ chỉ đọc được kích hoạt. Bạn có thể tiếp tục duyệt tới trang web, nhưng các tương tác có thể không hoạt động." + enabled: "Website đang ở chế độ chỉ đọc, bạn có thể duyệt xem nhưng không thể trả lời, likes, hay thực hiện các hành động khác." login_disabled: "Chức năng Đăng nhập đã bị tắt khi website trong trạng thái chỉ đọc" + too_few_topics_and_posts_notice: "Hãy bắt đầu thảo luận! Hiện có %{currentTopics} / %{requiredTopics} chủ đề và %{currentPosts} / %{requiredPosts} bài viết. Khách ghé thăm cần một số chủ đề để đọc và trả lời." + too_few_topics_notice: "Hãy bắt đầu thảo luận! Hiện có %{currentTopics} / %{requiredTopics} chủ đề. Khách ghé thăm cần một số chủ đề để đọc và trả lời." + too_few_posts_notice: "Hãy bắt đầu thảo luận! Hiện có %{currentPosts} / %{requiredPosts} bài viết. Khách ghé thăm cần một số chủ đề để đọc và trả lời." learn_more: "tìm hiểu thêm..." year: 'năm' year_desc: 'chủ đề được tạo ra trong 365 ngày qua' @@ -612,6 +693,9 @@ vi: sign_up: "Đăng ký" hide_session: "Nhắc vào ngày mai" hide_forever: "không, cảm ơn" + hidden_for_session: "OK, Tôi sẽ hỏi bạn vào ngày mai. Bạn có thể luôn luôn sử dụng chức năng đăng nhập để tạo tài khoản." + intro: "Xin chào! :heart_eyes: Có vẻ như bạn đang thích thú để thảo luận, nhưng bạn chưa đăng nhập." + value_prop: "Khi bạn tạo tài khoản, website nhớ chính xác những gì bạn đã đọc, vì vậy bạn sẽ luôn trở lại đúng nơi đã rời đi. Bạn cũng có thể nhận thông báo ở đây hoặc qua email mỗi khi có bài viết mới. Bạn cũng có thể like bài viết để chia sẻ cảm xúc của mình. :heartbeat:" summary: enabled_description: "Bạn đang xem một bản tóm tắt của chủ đề này: các bài viết thú vị nhất được xác định bởi cộng đồng." description: "Có {{count}} trả lời" @@ -670,6 +754,8 @@ vi: resend_activation_email: "Bấm đây để gửi lại email kích hoạt" sent_activation_email_again: "Chúng tôi gửi email kích hoạt tới cho bạn ở {{currentEmail}}. Nó sẽ mất vài phút để đến; bạn nhớ check cả hồm thư spam nhe. " to_continue: "Vui lòng đăng nhập" + preferences: "Bạn cần phải đăng nhập để thay đổi cài đặt tài khoản." + forgot: "Tôi không thể nhớ lại chi tiết tài khoản của tôi." google: title: "với Google " message: "Chứng thực với Google (Bạn hãy chắc chắn là chặn popup không bật)" @@ -692,8 +778,13 @@ vi: google: "Google" twitter: "Twitter" emoji_one: "Emoji One" + shortcut_modifier_key: + shift: 'Shift' + ctrl: 'Ctrl' + alt: 'Alt' composer: - emoji: "Emoji :smile:" + emoji: "Emoji :)" + more_emoji: "thêm..." options: "Lựa chọn" whisper: "nói chuyện" add_warning: "Đây là một cảnh báo chính thức" @@ -704,6 +795,7 @@ vi: saved_local_draft_tip: "Đã lưu locally" similar_topics: "Bài viết của bạn tương tự với " drafts_offline: "Nháp offline" + group_mentioned: "Bằng cách sử dụng {{group}}, bạn có thể thông báo tới {{count}} người." error: title_missing: "Tiêu đề là bắt buộc" title_too_short: "Tiêu để phải có ít nhất {{min}} ký tự" @@ -726,6 +818,7 @@ vi: show_edit_reason: "(thêm lý do sửa)" reply_placeholder: "Gõ ở đây. Sử dụng Markdown, BBCode, hoặc HTML để định dạng. Kéo hoặc dán ảnh." view_new_post: "Xem bài đăng mới của bạn. " + saving: "Đang lưu" saved: "Đã lưu" saved_draft: "Bài nháp đang lưu. Chọn để tiếp tục." uploading: "Đang đăng " @@ -740,6 +833,7 @@ vi: link_description: "Nhập mô tả liên kết ở đây" link_dialog_title: "Chèn liên kết" link_optional_text: "tiêu đề tùy chọn" + link_placeholder: "http://example.com \"chữ tuỳ chọn\"" quote_title: "Trích dẫn" quote_text: "Trích dẫn" code_title: "Văn bản định dạng trước" @@ -754,7 +848,9 @@ vi: hr_title: "Căn ngang" help: "Trợ giúp soạn thảo bằng Markdown" toggler: "ẩn hoặc hiển thị bảng điều khiển soạn thảo" + modal_ok: "OK" modal_cancel: "Hủy" + cant_send_pm: "Xin lỗi, bạn không thể gởi tin nhắn đến %{username}." admin_options_title: "Tùy chọn quản trị viên cho chủ đề này" auto_close: label: "Thời gian tự khóa chủ đề:" @@ -771,6 +867,7 @@ vi: more: "xem thông báo cũ hơn" total_flagged: "tổng số bài viết gắn cờ" mentioned: "

      {{username}} {{description}}

      " + group_mentioned: "

      {{username}} {{description}}

      " quoted: "

      {{username}} {{description}}

      " replied: "

      {{username}} {{description}}

      " posted: "

      {{username}} {{description}}

      " @@ -783,6 +880,8 @@ vi: moved_post: "

      {{username}} chuyển {{description}}

      " linked: "

      {{username}} {{description}}

      " granted_badge: "

      Thu được '{{description}}'

      " + group_message_summary: + other: "

      {{count}} tin nhắn trong {{group_name}} của bạn

      " alt: mentioned: "Được nhắc đến bởi" quoted: "Trích dẫn bởi" @@ -791,11 +890,16 @@ vi: edited: "Bài viết của bạn được sửa bởi" liked: "Bạn đã like bài viết" private_message: "Tin nhắn riêng từ" + invited_to_private_message: "Lời mời thảo luận riêng từ" + invited_to_topic: "Lời mời tham gia chủ đề từ" invitee_accepted: "Lời mời được chấp nhận bởi" moved_post: "Bài viết của bạn đã được di chuyển bởi" linked: "Liên kết đến bài viết của bạn" + granted_badge: "Cấp huy hiệu" + group_message_summary: "Tin nhắn trong hộp thư đến" popup: mentioned: '{{username}} nhắc đến bạn trong "{{topic}}" - {{site_title}}' + group_mentioned: '{{username}} nhắc đến bạn trong "{{topic}}" - {{site_title}}' quoted: '{{username}} trích lời bạn trong "{{topic}}" - {{site_title}}' replied: '{{username}} trả lời cho bạn trong "{{topic}}" - {{site_title}}' posted: '{{username}} gửi bài trong "{{topic}}" - {{site_title}}' @@ -807,7 +911,9 @@ vi: from_my_computer: "Từ thiết bị của tôi" from_the_web: "Từ Web" remote_tip: "đường dẫn tới hình ảnh" + remote_tip_with_attachments: "chọn ảnh hoặc file {{authorized_extensions}}" local_tip: "chọn hình từ thiết bị của bạn" + local_tip_with_attachments: "chọn ảnh hoặc file {{authorized_extensions}} từ thiết bị của bạn" hint: "(Bạn cũng có thể kéo & thả vào trình soạn thảo để tải chúng lên)" hint_for_supported_browsers: "bạn có thể kéo và thả ảnh vào trình soan thảo này" uploading: "Đang tải lên" @@ -841,9 +947,17 @@ vi: current_user: 'đi đến trang cá nhân của bạn' topics: bulk: + unlist_topics: "Chủ đề không công khai" reset_read: "Đặt lại lượt đọc" delete: "Xóa chủ đề" + dismiss: "Bỏ qua" + dismiss_read: "Bỏ qua tất cả thư chưa đọc" + dismiss_button: "Bỏ qua..." + dismiss_tooltip: "Bỏ qua chỉ bài viết mới hoặc ngừng theo dõi chủ đề" + also_dismiss_topics: "Ngừng theo dõi các chủ đề này để không hiển thị lại là chủ đề chưa đọc" dismiss_new: "Bỏ " + toggle: "chuyển sang chọn chủ đề theo lô" + actions: "Hành động theo lô" change_category: "Chuyển chuyên mục" close_topics: "Đóng các chủ đề" archive_topics: "Chủ đề Lưu trữ" @@ -884,6 +998,12 @@ vi: create: 'Chủ đề Mới' create_long: 'Tạo một Chủ đề mới' private_message: 'Bắt đầu một thông điệp' + archive_message: + help: 'Chuyển tin nhắn sang lưu trữ' + title: 'Lưu trữ' + move_to_inbox: + title: 'Chuyển sang hộp thư' + help: 'Chuyển tin nhắn trở lại hộp thư' list: 'Chủ đề' new: 'chủ đề mới' unread: 'chưa đọc' @@ -928,6 +1048,7 @@ vi: auto_close_title: 'Tự động-Đóng các Cài đặt' auto_close_save: "Lưu" auto_close_remove: "Đừng Tự Động-Đóng Chủ Đề Này" + auto_close_immediate: "Bài viết cuối cùng trong chủ đề đã xảy ra %{hours} giờ qua, vì vậy chủ đề sẽ tự động đóng." progress: title: tiến trình của chủ đề go_top: "trên cùng" @@ -968,13 +1089,16 @@ vi: description: "Một số trả lời mới sẽ được hiển thị trong chủ đề này. Bạn sẽ được thông báo nếu ai đó đề cập đến @tên của bạn hoặc trả lời bạn" regular: title: "Bình thường" + description: "Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn" regular_pm: title: "Bình thường" + description: "Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn" muted_pm: title: "Im lặng" description: "Bạn sẽ không bao giờ được thông báo về bất cứ điều gì về tin nhắn này. " muted: title: "Im lặng" + description: "Bạn sẽ không nhận được bất kỳ thông báo nào trong chủ đề này, và chúng sẽ không hiển thị là mới nhất." actions: recover: "Không-Xóa Chủ Đề Này" delete: "Xóa-Chủ Đề Này" @@ -986,6 +1110,8 @@ vi: unpin: "Bỏ-Ghim Chủ Đề..." unarchive: "Chủ đề Không Lưu Trữ" archive: "Chủ Đề Lưu Trữ" + invisible: "Make Unlisted" + visible: "Make Listed" reset_read: "Đặt lại dữ liệu đọc" feature: pin: "Ghim Chủ Đề" @@ -1007,26 +1133,56 @@ vi: help: 'đánh dấu riêng tư chủ đề này cho sự chú ý hoặc gửi một thông báo riêng về nó' success_message: 'Bạn đã đánh dấu thành công chủ đề này' feature_topic: + title: "Đề cao chủ đề này" + pin: "Làm cho chủ đề này xuất hiện trên top của chuyên mục {{categoryLink}}" confirm_pin: "Bạn đã có {{count}} chủ đề được ghim. Qúa nhiều chủ đề được ghim có thể là một trở ngại cho những thành viên mới và thành viên ẩn danh. Bạn có chắc chắn muốn ghim chủ đề khác trong chuyên mục này?" unpin: "Xóa chủ đề này từ phần trên cùng của chủ đề {{categoryLink}}" + unpin_until: "Gỡ bỏ chủ đề này khỏi top của chuyên mục {{categoryLink}} và đợi cho đến %{until}." pin_note: "Người dùng có thể bỏ ghim chủ đề riêng cho mình" pin_validation: "Ngày được yêu câu để gắn chủ đề này" + not_pinned: "Không có chủ đề được ghim trong {{categoryLink}}." + already_pinned: + other: "Chủ đề gần đây được ghim trong {{categoryLink}}: {{count}}" + pin_globally: "Làm cho chủ đề này xuất hiện trên top của tất cả các chủ đề" + confirm_pin_globally: "Bạn đã có {{count}} chủ đề được ghim. Ghim quá nhiều chủ đề có thể là trở ngại cho những thành viên mới và ẩn danh. Bạn có chắc chắn muốn ghim chủ đề khác?" unpin_globally: "Bỏ chủ đề này khỏi phần trên cùng của danh sách tất cả các chủ đề" + unpin_globally_until: "Gỡ bỏ chủ đề này khỏi top của danh sách tất cả các chủ đề và đợi cho đến %{until}." global_pin_note: "Người dùng có thể bỏ ghim chủ đề riêng cho mình" + not_pinned_globally: "Không có chủ đề nào được ghim." + already_pinned_globally: + other: "Chủ đề gần đây được ghim trong: {{count}}" + make_banner: "Đặt chủ đề này là một banner xuất hiện trên top của tất cả các trang." + remove_banner: "Gỡ bỏ banner xuất hiện trên top của tất cả các trang." + banner_note: "Người dùng có thể bỏ qua banner này bằng cách đóng nó. Chỉ một chủ đề có thể được đặt là banner tại một thời điểm." + no_banner_exists: "Không có chủ đề banner nào." + banner_exists: "Có is đang là chủ đề banner." inviting: "Đang mời..." + automatically_add_to_groups_optional: "Lời mời này cũng bao gồm quyền truy cập vào các nhóm: (optional, admin only)" + automatically_add_to_groups_required: "Lời mời này cũng bao gồm quyền truy cập vào các nhóm: (Required, admin only)" invite_private: + title: 'Mời thảo luận' + email_or_username: "Email hoặc username người được mời" email_or_username_placeholder: "địa chỉ thư điện tử hoặc tên người dùng" action: "Mời" + success: "Chúng tôi đã mời người đó tham gia thảo luận này." error: "Xin lỗi, có lỗi khi mời người dùng này." group_name: "Nhóm tên" + controls: "Topic Controls" invite_reply: title: 'Mời' username_placeholder: "tên người dùng" action: 'Gửi Lời Mời' + help: 'mời người khác tham gia chủ đề thông qua email hoặc thông báo' to_forum: "Chúng tôi sẽ gửi một email tóm tắt cho phép bạn của bạn gia nhập trực tiệp bằng cách nhấp chuột vào một đường dẫn, không cần phải đăng nhập." sso_enabled: "Nhập tên đăng nhập hoặc địa chỉ email của người mà bạn muốn mời vào chủ đề này." to_topic_blank: "Nhập tên đăng nhập hoặc địa chỉ email của người bạn muốn mời đến chủ đề này." + to_topic_email: "Bạn vừa điền địa chỉ email, website sẽ gửi lời mời cho phép bạn bè của bạn có thể trả lời chủ đề này." + to_topic_username: "Bạn vừa điền tên thành viên, website sẽ gửi thông báo kèm theo lời mời họ tham gia chủ đề này." + to_username: "Điền tên thành viên bạn muốn mời, website sẽ gửi thông báo kèm theo lời mời họ tham gia chủ đề này." email_placeholder: 'name@example.com' + success_email: "Website vừa gửi lời mời tới {{emailOrUsername}} và sẽ thông báo cho bạn khi lời mời đó được chấp nhận. Kiểm tra tab lời mời trên trang tài khoản để theo dõi lời mời của bạn." + success_username: "Website đã mời người đó tham gia thảo luận này." + error: "Xin lỗi, chúng tôi không thể mời người đó. Có lẽ họ đã được mời? (giới hạn lời mời)" login_reply: 'Đăng nhập để trả lời' filters: n_posts: @@ -1037,20 +1193,29 @@ vi: action: "di chuyển tới chủ đề mới" topic_name: "Tên chủ đề mới" error: "Có lỗi khi di chuyển bài viết tới chủ đề mới." + instructions: + other: "Bạn muốn tạo chủ đề mới và phổ biến nó với {{count}} bài viết đã chọn." merge_topic: title: "Di chuyển tới chủ đề đang tồn tại" action: "di chuyển tới chủ đề đang tồn tại" error: "Có lỗi khi di chuyển bài viết đến chủ đề này." + instructions: + other: "Hãy chọn chủ đề bạn muốn di chuyển {{count}} bài viết này tới." change_owner: title: "Chuyển chủ sở hữu bài viết" action: "chuyển chủ sở hữu" + error: "Có lỗi xảy ra khi thay đổi quyền sở hữu của các bài viết." label: "Chủ sở hữ mới của Bài viết" placeholder: "tên đăng nhập của chủ sở hữu mới" + instructions: + other: "Hãy chọn chủ sở hữu mới cho {{count}} bài viết của {{old_user}}." + instructions_warn: "Lưu ý rằng bất kỳ thông báo nào về bài viết này sẽ không được chuyển giao cho thành viên mới trở về trước.
      Cảnh báo: Hiện không có dữ liệu bài viết phụ thuộc được chuyển giao cho thành viên mới. Hãy thận trọng!" change_timestamp: title: "Đổi Timestamp" action: "đổi timestamp" invalid_timestamp: "Timestamp không thể trong tương lai." error: "Có lỗi khi thay đổi timestamp của chủ đề." + instructions: "Hãy chọn dòng thời gian mới cho chủ đề, các bài viết trong chủ đề sẽ được cập nhật để có sự khác biệt cùng một lúc." multi_select: select: 'chọn' selected: 'đã chọn ({{count}})' @@ -1074,6 +1239,8 @@ vi: follow_quote: "đến bài viết trích dẫn" show_full: "Hiển thị đầy đủ bài viết" show_hidden: 'Xem nội dung ẩn' + deleted_by_author: + other: "(bài viết theo tác giả sẽ được xóa tự động sau %{count} giờ, trừ khi đã đánh dấu)" expand_collapse: "mở/đóng" gap: other: "xem {{count}} trả lời bị ẩn" @@ -1085,6 +1252,9 @@ vi: other: "{{count}} Thích" has_likes_title: other: "{{count}} người thích bài viết này" + has_likes_title_only_you: "bạn đã like bài viết này" + has_likes_title_you: + other: "bạn và {{count}} người khác đã like bài viết này" errors: create: "Xin lỗi, có lỗi xảy ra khi tạo bài viết của bạn. Vui lòng thử lại." edit: "Xin lỗi, có lỗi xảy ra khi sửa bài viết của bạn. Vui lòng thử lại." @@ -1104,7 +1274,7 @@ vi: via_email: "bài viết này đăng qua email" whisper: "bài viết này là lời nhắn từ điều hành viên" wiki: - about: "bài viết này là wiki; người dùng cơ bản có thể sửa nó" + about: "bài viết này là wiki" archetypes: save: 'Lưu lựa chọn' controls: @@ -1114,6 +1284,7 @@ vi: undo_like: "hủy like" edit: "sửa bài viết này" edit_anonymous: "Xin lỗi, nhưng bạn cần đăng nhập để sửa bài viết này." + flag: "đánh dấu bài viết này để tạo chú ý hoặc gửi một thông báo riêng về nó" delete: "xóa bài viết này" undelete: "hủy xóa bài viết này" share: "chia sẻ liên kết đến bài viết này" @@ -1123,14 +1294,18 @@ vi: other: "Bạn muốn xóa {{count}} trả lời cho bài viết này?" yes_value: "Đồng ý, xóa những trả lời" no_value: "Không, chỉ xóa chủ đề" + admin: "quản lý bài viết" wiki: "Tạo Wiki" unwiki: "Xóa Wiki" convert_to_moderator: "Thêm màu Nhân viên" revert_to_regular: "Xóa màu Nhân viên" rebake: "Tạo lại HTML" unhide: "Bỏ ẩn" + change_owner: "Đổi chủ sở hữu" actions: flag: 'Gắn cờ' + defer_flags: + other: "Đánh dấu hoãn" it_too: off_topic: "Gắn cờ nó" spam: "Gắn cờ nó" @@ -1191,6 +1366,8 @@ vi: other: "{{count}} người khác đánh dấu là rác" inappropriate: other: "{{count}} người khác đã đánh dấu là không phù hợp" + notify_moderators: + other: "{{count}} người đã đánh dấu để chờ duyệt" notify_user: other: "{{count}} gửi tin nhắn đến người dùng này" bookmark: @@ -1210,6 +1387,7 @@ vi: last: "Sửa đổi gần nhất" hide: "Ẩn sửa đổi" show: "Hiện sửa đổi" + comparing_previous_to_current_out_of_total: "{{previous}} {{current}} / {{total}}" displays: inline: button: ' HTML' @@ -1221,6 +1399,7 @@ vi: can: 'can…' none: '(không danh mục)' all: 'Tất cả danh mục' + choose: 'Chọn chuyên mục…' edit: 'sửa' edit_long: "Sửa" view: 'Xem Chủ đề trong Danh mục' @@ -1231,6 +1410,8 @@ vi: create: 'Chuyên mục mới' create_long: 'Tạo Chủ đề mới' save: 'Lưu chuyên mục' + slug: 'Đường dẫn chuyên mục' + slug_placeholder: '(Tùy chọn) các từ sử dụng trong url' creation_error: Có lỗi xảy ra khi tạo chuyên mục save_error: Có lỗi xảy ra khi lưu chuyên mục name: "Tên chuyên mục" @@ -1238,7 +1419,9 @@ vi: topic: "chủ đề chuyên mục" logo: "Logo của chuyên mục" background_image: "Ảnh nền của chuyên mục" + badge_colors: "Màu huy hiệu" background_color: "Màu nền" + foreground_color: "Màu mặt trước" name_placeholder: "Tối đa một hoặc hai từ" color_placeholder: "Bất cứ màu nào" delete_confirm: "Bạn có chắc sẽ xóa chuyên mục này chứ?" @@ -1248,46 +1431,71 @@ vi: change_in_category_topic: "Sửa mô tả" already_used: 'Màu này đã được dùng bởi chuyên mục khác' security: "Bảo mật" + special_warning: "Cảnh báo: Đây là chuyên mục có sẵn nên bạn không thể chỉnh sửa các thiết lập bảo mật. Nếu bạn muốn sử dụng chuyên mục này, hãy xóa nó thay vì tái sử dụng." images: "Hình ảnh" auto_close_label: "Tự động khóa chủ đề sau:" auto_close_units: "giờ" email_in: "Tùy chỉnh địa chỉ nhận thư điện tử " email_in_allow_strangers: "Nhận thư điện tử từ người gửi vô danh không tài khoản" + email_in_disabled: "Tạo chủ đề mới thông qua email đã được tắt trong thiết lập. Để bật tính năng này, " email_in_disabled_click: 'kích hoạt thiết lập thư điện tử' + suppress_from_homepage: "Ngăn chặn chuyên mục này hiển thị trên trang chủ." allow_badges_label: "Cho phép thưởng huy hiệu trong chuyên mục này" edit_permissions: "Sửa quyền" add_permission: "Thêm quyền" this_year: "năm nay" position: "vị trí" default_position: "vị trí mặc định" + position_disabled: "Chuyên mục sẽ được hiển thị theo thứ tự hoạt động. Để kiểm soát thứ tự chuyên mục trong danh sách, " + position_disabled_click: 'bật thiết lập "cố định vị trí chuyên mục".' parent: "Danh mục cha" notifications: watching: title: "Theo dõi" + description: "Bạn sẽ tự động xem tất cả các chủ đề mới trong các chuyên mục này. Bạn sẽ được thông báo về tất các các chủ đề mới, và một số bài viết mới sẽ được hiển thị." tracking: title: "Đang theo dõi" + description: "Bạn sẽ tự động theo dõi tất cả các chủ đề mới trong các chuyên mục này. Bạn sẽ được thông báo nếu ai đó đề cập đến @tên của bạn hoặc trả lời bạn, và một số bài viết mới sẽ được hiển thị." regular: title: "Bình thường" + description: "Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn" muted: title: "Im lặng" + description: "Bạn sẽ không nhận được thông báo về bất cứ chủ đề mới nào trong các chuyên mục này, và chúng sẽ không hiển thị là mới nhất." flagging: + title: 'Cám ơn bạn đã giúp phát triển cộng đồng!' + private_reminder: 'đánh dấu là riêng tư, chỉ hiển thị với quản trị viên' action: 'Đánh dấu Bài viết' + take_action: "Thực hiện" notify_action: 'Tin nhắn' delete_spammer: "Xóa người Spam" delete_confirm: "Bạn đang định xóa %{posts} bài đăng và %{topics} chủ đề từ người dùng này, loại tài khoản, ngăn đăng ký từ địa chỉ IP %{ip_address} của họ, và thêm địa chỉ email %{email} vào danh sách chặn vĩnh viễn. Bạn có chắc người dùng này thật sự là một spammer?" + yes_delete_spammer: "Có, xóa người spam" ip_address_missing: "(N/A)" hidden_email_address: "(ẩn)" + submit_tooltip: "Đánh dấu riêng tư" + take_action_tooltip: "Tiếp cận ngưỡng đánh dấu ngay lập tức, thay vì đợi cộng đồng" + cant: "Xin lỗi, bạn không thể đánh dấu bài viết lúc này." + notify_staff: 'Thông báo quản trị viên' formatted_name: off_topic: "Nó là sai chủ đề" + inappropriate: "Không phù hợp" spam: "Nó là rác" + custom_placeholder_notify_user: "Phải hảo tâm và mang tính xây dựng." + custom_placeholder_notify_moderators: "Hãy cho chúng tôi biết cụ thể những gì bạn quan tâm, và cung cấp các liên kết hoặc ví dụ liên quan nếu có thể." custom_message: + at_least: "điền ít nhất {{n}} ký tự" more: "còn {{n}}" + left: "{{n}} còn lại" flagging_topic: + title: "Cám ơn bạn đã giúp phát triển cộng đồng!" action: "Gắn cờ Chủ đề" notify_action: "Tin nhắn" topic_map: title: "Tóm tắt Chủ đề" + participants_title: "Poster thường xuyên" links_title: "Liên kết phổ biến" + links_shown: "hiện tất cả {{totalLinks}} liên kết..." clicks: other: "%{count} nhấp chuột" topic_statuses: @@ -1297,10 +1505,21 @@ vi: help: "Bạn đã đánh dấu chủ đề này" locked: help: "Chủ đề đã đóng; không cho phép trả lời mới" + archived: + help: "Chủ đề này đã được lưu trữ, bạn không thể sửa đổi nữa" + locked_and_archived: + help: "Chủ đề này đã đóng và lưu trữ, không cho phép trả lời mới và sửa đổi nữa" unpinned: title: "Hủy gắn" + help: "Chủ đề này không còn được ghim nữa, nó sẽ hiển thị theo thứ tự thông thường" + pinned_globally: + title: "Ghim toàn trang" + help: "Chủ đề này được ghim toàn trang, nó sẽ hiển thị ở trên cùng các chủ đề mới và trong chuyên mục" pinned: title: "Gắn" + help: "Chủ đề này đã được ghim, nó sẽ hiển thị ở trên cùng chuyên mục" + invisible: + help: "Chủ đề này ẩn, nó sẽ không hiển thị trong danh sách chủ đề, và chỉ có thể truy cập thông qua liên kết trực tiếp" posts: "Bài viết" posts_lowercase: "bài viết" posts_long: "Có {{number}} bài đăng trong chủ đề này" @@ -1335,19 +1554,37 @@ vi: with_topics: "%{filter} chủ đề" with_category: "%{filter} %{category} chủ đề" latest: + title: "Mới nhất" + title_with_count: + other: "Mới nhất ({{count}})" help: "chủ đề với bài viết gần nhất" hot: title: "Nổi bật" + help: "chọn các chủ đề nóng nhất" read: title: "Đọc" + help: "chủ đề bạn đã đọc, theo thứ tự bạn đọc lần cuối cùng" search: title: "Tìm kiếm" help: "tìm trong tất cả chủ đề" categories: title: "Danh mục" title_in: "Danh mục - {{categoryName}}" + help: "tất cả các chủ đề được nhóm theo chuyên mục" + unread: + title: "Chưa đọc" + title_with_count: + other: "Chưa đọc ({{count}})" + help: "chủ đề bạn đang xem hoặc theo dõi có bài viết chưa đọc" + lower_title_with_count: + other: "{{count}} chưa đọc" new: + lower_title_with_count: + other: "{{count}} mới" lower_title: "mới" + title: "Mới" + title_with_count: + other: "Mới ({{count}})" help: "chủ đề đã tạo cách đây vài ngày" posted: title: "Bài viết của tôi" @@ -1356,9 +1593,13 @@ vi: title: "Đánh dấu" help: "chủ để của bạn đã được đánh dấu" category: + title: "{{categoryName}}" + title_with_count: + other: "{{categoryName}} ({{count}})" help: "Những chủ đề mới nhất trong chuyên mục{{categoryName}} " top: title: "Trên" + help: "Các chủ đề tích cực nhất trong năm, tháng, tuần, hoặc ngày trước" all: title: "Từ trước tới nay" yearly: @@ -1378,6 +1619,7 @@ vi: this_week: "Tuần" today: "Ngày" other_periods: "xem top" + browser_update: 'Không may, trình duyệt của bạn quá cũ để website hoạt động. Hãy nâng cấp trình duyệt của bạn.' permission_types: full: "Tạo / Trả lời / Xem" create_post: "Trả lời / Xem" @@ -1395,6 +1637,9 @@ vi: critical_available: "Bản cập nhật quan trọng sẵn sằng." updates_available: "Cập nhật đang sẵng sàng" please_upgrade: "Vui lòng cập nhật!" + no_check_performed: "Kiểm tra phiên bản mới đã không được thực hiện, đảm bảo rằng Sidekiq đang chạy." + stale_data: "Kiểm tra phiên bản mới đã không được thực hiện gần đây, đảm bảo rằng Sidekiq đang chạy." + version_check_pending: "Hình như bạn mới nâng cấp, thật tuyệt!" installed_version: "Đã cài đặt" latest_version: "Mới nhất" problems_found: "Tìm thấy vấn đề với bản cài đặt Discourse của bạn:" @@ -1430,6 +1675,7 @@ vi: refresh_report: "Làm mới báo cáo" start_date: "Từ ngày" end_date: "Đến ngày" + groups: "Các nhóm" commits: latest_changes: "Thay đổi cuối: vui lòng cập nhật thường xuyên!" by: "bởi" @@ -1438,6 +1684,7 @@ vi: old: "Cũ" active: "Kích hoạt" agree: "Đồng ý" + agree_title: "Xác nhận đánh dấu này hợp lệ và chính xác" agree_flag_modal_title: "Đồng ý và..." agree_flag_hide_post: "Đồng ý (ẩn bài viết + gửi PM)" agree_flag_hide_post_title: "Ẩn bài viết này và tự động gửi tin nhắn đến người dùng hối thúc họ sửa nó" @@ -1448,6 +1695,8 @@ vi: defer_flag: "Hoãn" defer_flag_title: "Xóa cờ này; nó yêu cầu không có hành động nào vào thời điểm này." delete: "Xóa" + delete_title: "Xóa bài viết đánh dấu này đề cập đến." + delete_post_defer_flag: "Xóa bài viết và Hoãn đánh dấu" delete_post_defer_flag_title: "Xóa bài viết; nếu là bài viết đầu tiên, xóa chủ đề này" delete_post_agree_flag: "Xóa bài viết và Đồng ý với cờ" delete_post_agree_flag_title: "Xóa bài viết; nếu là bài viết đầu tiên, xóa chủ đề này" @@ -1455,21 +1704,38 @@ vi: delete_spammer: "Xóa người Spam" delete_spammer_title: "Xóa người dùng này và tất cả bài viết à chủ để của người dùng này." disagree_flag_unhide_post: "Không đồng ý (ẩn bài viết)" + disagree_flag_unhide_post_title: "Loại bỏ bất kỳ đánh dấu nào khỏi bài viết này và làm cho bài viết hiển thị trở lại" disagree_flag: "Không đồng ý" + disagree_flag_title: "Từ chối đánh dấu này là không hợp lệ hoặc chính xác" clear_topic_flags: "Hoàn tất" + clear_topic_flags_title: "Chủ đề đã được xem xét vấn đề và giải quyết, click để loại bỏ đánh dấu." more: "(thêm trả lời...)" dispositions: agreed: "đồng ý" disagreed: "không đồng ý" deferred: "hoãn" flagged_by: "Gắn cờ bởi" + resolved_by: "Xử lý bởi" + took_action: "Thực hiện" system: "Hệ thống" error: "Có lỗi xảy ra" reply_message: "Trả lời " no_results: "Không được gắn cờ" + topic_flagged: "Chủ đề này đã được đánh dấu." + visit_topic: "Tới chủ đề để thực hiện" + was_edited: "Bài viết đã được chỉnh sửa sau khi đánh dấu đầu tiên" + previous_flags_count: "Bài viết này đã được đánh dấu {{count}} lần." summary: action_type_3: other: "sai chủ đề x{{count}}" + action_type_4: + other: "Không phù hợp x{{count}}" + action_type_6: + other: "tùy chỉnh x{{count}}" + action_type_7: + other: "tùy chỉnh x{{count}}" + action_type_8: + other: "spam x{{count}}" groups: primary: "Nhóm Chính" no_primary: "(không có nhóm chính)" @@ -1479,15 +1745,30 @@ vi: new: "Mới" selector_placeholder: "nhập tên tài khoản" name_placeholder: "Tên nhóm, không khoản trắng, cùng luật với tên tài khoản" + about: "Chỉnh sửa nhóm thành viên và tên của bạn ở đây" group_members: "Nhóm thành viên" delete: "Xóa" delete_confirm: "Xóa nhóm này?" + delete_failed: "Không thể xóa nhóm. Nếu đây là một nhóm tự động, nó không thể hủy bỏ." + delete_member_confirm: "Loại bỏ '%{username}' khỏi nhóm '%{group}'?" + delete_owner_confirm: "Loại bỏ quyền sở hữu của '%{username}'?" name: "Tên" add: "Thêm" add_members: "Thêm thành viên" custom: "Tùy biến" + bulk_complete: "Các thành viên đã được thêm vào nhóm." + bulk: "Thêm vào nhóm theo lô" + bulk_paste: "Dán danh sách username hoặc email, mỗi mục một dòng:" + bulk_select: "(chọn nhóm)" automatic: "Tự động" + automatic_membership_email_domains: "Các thành viên đã đăng ký với đuôi email khớp với một trong danh sách này sẽ được tự động thêm vào nhóm:" + automatic_membership_retroactive: "Áp dụng quy tắc đuôi email tương tự để thêm thành viên đăng ký hiện tại" + default_title: "Tên mặc định cho tất cả các thành viên trong nhóm này" primary_group: "Tự động cài là nhóm chính" + group_owners: Chủ sở hữu + add_owners: Thêm chủ sở hữu + incoming_email: "Tùy chỉnh địa chỉ email đến" + incoming_email_placeholder: "điền địa chỉ email" api: generate_master: "Tạo Master API Key" none: "Không có API keys nào kích hoạt lúc này." @@ -1498,6 +1779,8 @@ vi: regenerate: "Khởi tạo lại" revoke: "Thu hồi" confirm_regen: "Bạn muốn thay API Key hiện tại bằng cái mới?" + confirm_revoke: "Bạn có chắc chắn muốn hủy bỏ khóa đó?" + info_html: "Khóa API cho phép bạn tạo và cập nhật chủ đề sử dụng JSON." all_users: "Tất cả Thành viên" note_html: "Giữ khóa nào bảo mật, tất cả tài khoản có thể dùng khóa này để tạo bài viết với bất kỳ tài khoản nào." plugins: @@ -1562,13 +1845,20 @@ vi: confirm: "Bạn muốn khôi phục bản sao lưu này?" rollback: label: "Rollback" + title: "Đưa csdl về trạng thái làm việc trước" + confirm: "Bạn có chắc chắn muốn đưa csdl về trạng thái làm việc trước?" export_csv: + user_archive_confirm: "Bạn có chắc chắn muốn download các bài viết của mình?" + success: "Export đang được khởi tạo, bạn sẽ nhận được tin nhắn thông báo khi quá trình hoàn tất." failed: "Xuất lỗi. Vui lòng kiểm tra log." rate_limit_error: "Bài viết có thể tải về 1 lần mỗi này, vui lòng thử lại vào ngày mai." button_text: "Xuất" button_title: user: "Xuất danh sách người dùng đầy đủ với định dạng CSV." staff_action: "Xuất đầy đủ log hành động của nhân viên với định dạng CSV." + screened_email: "Export danh sách email theo định dạng CSV." + screened_ip: "Export danh sách IP theo định dạng CSV." + screened_url: "Export danh sách URL theo định dạng CSV." export_json: button_text: "Xuất" invite: @@ -1592,8 +1882,13 @@ vi: enabled: "Cho phép?" preview: "xem trước" undo_preview: "xóa xem trước" + rescue_preview: "default style" + explain_preview: "Xem website với stylesheet tùy chỉnh" + explain_undo_preview: "Quay trở lại với kiểu tùy chỉnh stylesheet hiện tại" + explain_rescue_preview: "Xem website với stylesheet mặc định" save: "Lưu" new: "Mới" + new_style: "Style mới" import: "Nhập" import_title: "Chọn một file hoặc paste chữ." delete: "Xóa" @@ -1602,6 +1897,14 @@ vi: color: "Màu sắc" opacity: "Độ mờ" copy: "Sao chép" + email_templates: + title: "Email Templates" + subject: "Chủ đề" + multiple_subjects: "Email template này có nhiều chủ đề." + body: "Nội dung" + none_selected: "Chọn email template để bắt đầu chỉnh sửa." + revert: "Hoàn nguyên thay đổi" + revert_confirm: "Bạn có chắc chắn muốn hoàn nguyên các thay đổi?" css_html: title: "CSS/HTML" long_title: "Tùy biến CSS và HTML" @@ -1625,31 +1928,42 @@ vi: tertiary: name: 'cấp ba' description: 'Liên kết, một và nút, thông báo, và màu nhấn.' + quaternary: + name: "chia bốn" + description: "Liên kết điều hướng." header_background: name: "nền header" description: "Màu nền header của trang." header_primary: name: "header chính" + description: "Chữ và icon trong header của website." highlight: name: 'highlight' + description: 'Màu nền của các thành phần được đánh dấu trên trang, như là bài viết và chủ đề.' danger: name: 'nguy hiểm' + description: 'Màu đánh dấu cho thao tác xóa bài viết và chủ đề.' success: name: 'thành công' + description: 'Sử dụng để chỉ một thao tác đã thành công.' love: name: 'đáng yêu' description: "Màu của nút like" wiki: name: 'wiki' + description: "Màu cơ bản sử dụng cho nền của các bài viết wiki." email: - title: "Email" + title: "Emails" settings: "Cấu hình" - all: "Tất cả" + templates: "Templates" + preview_digest: "Xem trước tập san" sending_test: "Đang gửi Email test..." error: "LỖI - %{server_error}" test_error: "Có vấn đề khi gửi email test. Vui lòng kiểm tra lại cấu hình email của bạn, chắc chắn host mail của bạn không bị khóa kết nối, và thử lại." sent: "Đã gửi" skipped: "Đã bỏ qua" + received: "Đã nhận" + rejected: "Từ chối" sent_at: "Đã gửi vào lúc" time: "Thời gian" user: "Thành viên" @@ -1658,6 +1972,8 @@ vi: test_email_address: "địa chỉ email để test" send_test: "Gửi Email test" sent_test: "đã gửi!" + delivery_method: "Phương thức chuyển giao" + preview_digest_desc: "Xem trước nội dung của tập san email đã gửi cho các thành viên không hoạt động." refresh: "Tải lại" format: "Định dạng" html: "html" @@ -1665,18 +1981,34 @@ vi: last_seen_user: "Người dùng cuối:" reply_key: "Key phản hồi" skipped_reason: "Bỏ qua Lý do" + incoming_emails: + from_address: "Từ" + to_addresses: "Tới" + cc_addresses: "Cc" + subject: "Chủ đề" + error: "Lỗi" + none: "Không tìm tháy các email đến." + filters: + from_placeholder: "from@example.com" + to_placeholder: "to@example.com" + cc_placeholder: "cc@example.com" + subject_placeholder: "Chủ đề..." + error_placeholder: "Lỗi" logs: none: "Không tìm thấy log." filters: title: "Lọc" user_placeholder: "tên người dùng" address_placeholder: "name@example.com" + type_placeholder: "tập san, đăng ký..." reply_key_placeholder: "key phản hồi" skipped_reason_placeholder: "lý do" logs: title: "Log" action: "Hành động" created_at: "Đã tạo" + last_match_at: "Khớp lần cuối" + match_count: "Khớp" ip_address: "IP" topic_id: "ID Chủ đề" post_id: "ID Bài viết" @@ -1688,16 +2020,22 @@ vi: block: "khóa" do_nothing: "không làm gì" staff_actions: + title: "Staff Actions" + instructions: "Click username và thực hiện lọc danh sách, click ảnh hồ sơ để đến trang thành viên." clear_filters: "Hiện thị mọi thứ" staff_user: "Tài khoản Nhân viên" + target_user: "Target User" subject: "Chủ đề" when: "Khi" + context: "Ngữ cảnh" details: "Chi tiết" previous_value: "Trước" new_value: "Mới" diff: "So sánh" show: "Hiển thị" modal_title: "Chi tiết" + no_previous: "Không có giá trị trước đó." + deleted: "Không có giá trị mới, bản ghi đã được xóa." actions: delete_user: "xóa người dùng" change_trust_level: "thay đổi cấp tin cậy" @@ -1705,20 +2043,43 @@ vi: change_site_setting: "thay đổi cấu hình trang" change_site_customization: "thay đổi tùy biến trang" delete_site_customization: "xóa tùy biến trang" + change_site_text: "thay đổi chữ trên website" + suspend_user: "tạm khóa thành viên" + unsuspend_user: "hủy tạm khóa thành viên" + grant_badge: "cấp huy hiệu" + revoke_badge: "hủy bỏ huy hiệu" check_email: "kiểm tra email" delete_topic: "xóa chủ đề" delete_post: "xóa bài viết" + impersonate: "mạo danh" + anonymize_user: "thành viên ẩn danh" + roll_up: "cuộn lên khối IP" change_category_settings: "thay đổi cấu hình danh mục" delete_category: "xóa danh mục" create_category: "tạo danh mục" + block_user: "khóa tài khoản" + unblock_user: "mở khóa tài khoản" + grant_admin: "cấp quản trị" + revoke_admin: "hủy bỏ quản trị" + grant_moderation: "cấp điều hành" + revoke_moderation: "hủy bỏ điều hành" screened_emails: + title: "Screened Emails" + description: "Khi ai đó cố gắng tạo tài khoản mới, các địa chỉ email sau sẽ được kiểm tra và đăng ký sẽ bị chặn, hoặc một số hành động khác được thực hiện." email: "Địa chỉ Email" actions: allow: "Cho phép" screened_urls: + title: "Screened URLs" + description: "Các URL được liệt kê ở đây được sử dụng trong các bài viết của người dùng đã được xác định là spammer." url: "URL" domain: "Tên miền" screened_ips: + title: "Screened IPs" + description: 'Các địa chỉ IP đã được xem, sử dụng "Cho phép" để tạo danh sách trắng các địa chỉ.' + delete_confirm: "Bạn có chắc chắn muốn xóa quy tắc cho %{ip_address}?" + roll_up_confirm: "Bạn có chắc chắn muốn cuộn các địa chỉ IP thông thường vào các mạng con?" + rolled_up_some_subnets: "Cuộn thành công các IP cấm vào các mạng con: %{subnets}." rolled_up_no_subnet: "Không có gì để cuộn lên." actions: block: "Khóa" @@ -1731,11 +2092,14 @@ vi: filter: "Tìm kiếm" roll_up: text: "Cuộn lên" + title: "Tạo mạng con mới các entry cấm nếu có ít nhất 'min_ban_entries_for_roll_up' entry." logster: title: "Log lỗi" impersonate: title: "Mạo danh" + help: "Sử dụng công cụ này để mạo danh một tài khoản thành viên cho mục đích gỡ lỗi, bạn sẽ phải đăng xuất sau khi hoàn tất." not_found: "Không tìm thấy người dùng này." + invalid: "Xin lỗi, bạn không thể mạo danh tài khoản đó." users: title: 'Tài khoản' create: 'Thêm tài khoản Quản trị' @@ -1751,6 +2115,7 @@ vi: staff: 'Nhân viên' suspended: 'Đã tạm khóa' blocked: 'Đã khóa' + suspect: 'Nghi ngờ' approved: "Đã duyệt?" approved_selected: other: "duyệt tài khoản ({{count}})" @@ -1762,49 +2127,69 @@ vi: pending: 'Hoãn Xem xét Tài khoản' newuser: 'Tài khoản ở Cấp độ Tin tưởng 0 (Tài khoản mới)' basic: 'Tài khoản ở Cấp độ Tin tưởng 1 (Tài khoản Cơ bản)' + member: 'Tài khoản ở Độ tin cậy mức 2 (Member)' + regular: 'Tài khoản ở Độ tin cậy mức 3 (Regular)' + leader: 'Tài khoản ở Độ tin cậy mức 4 (Leader)' staff: "Nhân viên" admins: 'Tài khoản Quản trị' moderators: 'Điều hành viên' blocked: 'Tài khoản Khóa' suspended: 'Tài khoản Tạm khóa' + suspect: 'Tài khoản đáng ngờ' reject_successful: other: "Từ chối thành công %{count} tài khoản." reject_failures: other: "Từ chối thất bại %{count} tài khoản." not_verified: "Chưa xác thực" check_email: + title: "Khám phá email của tài khoản này" text: "Hiển thị" user: + suspend_failed: "Có gì đó đã sai khi đình chỉ tài khoản này {{error}}" + unsuspend_failed: "Có gì đó sai khi gỡ bỏ đình chỉ tài khoản này {{error}}" + suspend_duration: "Tài khoản này sẽ bị đình chỉ bao lâu?" suspend_duration_units: "(ngày)" + suspend_reason_label: "Tại sao bạn bị đình chỉ? Dòng chữ hiển thị cho tất cả mọi người sẽ hiển thị trên trang hồ sơ tài khoản của người dùng này, và sẽ hiển thị cho thành viên khi họ đăng nhập, hãy viết ngắn." suspend_reason: "Lý do" suspended_by: "Tạm khóa bởi" delete_all_posts: "Xóa tất cả bài viết" + delete_all_posts_confirm: "Bạn có chắc chắn muốn xóa %{posts} bài viết và %{topics} chủ đề?" suspend: "Tạm khóa" unsuspend: "Đã mở khóa" suspended: "Đã tạm khóa?" moderator: "Mod?" admin: "Quản trị?" blocked: "Đã khóa?" + staged: "Cấp bậc?" show_admin_profile: "Quản trị" edit_title: "Sửa Tiêu đề" save_title: "Lưu Tiêu đề" + refresh_browsers: "Bắt buộc làm mới trình duyệt" refresh_browsers_message: "Tin nhắn đã gửi cho tất cả người dùng!" show_public_profile: "Hiển thị hồ sơ công khai" impersonate: 'Mạo danh' ip_lookup: "Tìm kiếm địa chỉ IP" log_out: "Đăng suất" logged_out: "Thành viên đã đăng xuất trên tất cả thiết bị" + revoke_admin: 'Thu hồi quản trị' + grant_admin: 'Cấp quản trị' + revoke_moderation: 'Thu hồi điều hành' + grant_moderation: 'Cấp điều hành' unblock: 'Mở khóa' block: 'Khóa' reputation: Danh tiếng permissions: Quyền activity: Hoạt động + like_count: Đã like / Nhận last_100_days: 'trong 100 ngày gần đây' private_topics_count: Chủ đề riêng tư posts_read_count: Đọc bài viết post_count: Bài đăng đã được tạo topics_entered: Chủ để đã xem + flags_given_count: Đã đánh dấu + flags_received_count: Flags Received warnings_received_count: Đã nhận Cảnh báo + flags_given_received_count: 'Đã đánh dấu / Nhận' approve: 'Duyệt' approved_by: "duyệt bởi" approve_success: "Thành viên được duyệt và đã gửi email hướng đẫn kích hoạt." @@ -1817,6 +2202,12 @@ vi: delete: "Xóa thành viên" delete_forbidden_because_staff: "Admin và mod không thể xóa." delete_posts_forbidden_because_staff: "Không thể xóa tất cả bài viết của quản trị và điều hành viên." + delete_forbidden: + other: "Không thể xóa tài khoản nếu họ có bài viết, hãy xóa tất cả các bài viết trước khi xóa tài khoản. (Không thể xóa các bài viết cũ hơn %{count} ngày.)" + cant_delete_all_posts: + other: "Không thể xóa tất cả các bài viết, một số bài viết cũ hơn %{count} ngày. (Thiết lập delete_user_max_post_age.)" + cant_delete_all_too_many_posts: + other: "Không thể xóa tất cả các bài viết do tài khoản có hơn %{count} bài viết. (delete_all_posts_max)" delete_confirm: "Bạn CHẮC CHẮN muốn xóa thành viên này? Nó là vĩnh viễn!" delete_and_block: "Xóa và khóa email này và địa chỉ IP" delete_dont_block: "Chỉ xóa" @@ -1831,22 +2222,45 @@ vi: deactivate_failed: "Có vấn đề khi bỏ kích hoạt thành viên này." unblock_failed: 'Có vẫn đề khi gỡ khóa thành viên này.' block_failed: 'Có vấn đề khi khóa thành viên này.' + block_confirm: 'Bạn có chắc chắn muốn chặn người dùng này? Họ sẽ không thể tạo bất kỳ chủ đề hoặc bài viết mới nào.' + block_accept: 'Có, chặn người dùng này' + deactivate_explanation: "Tài khoản chờ kích hoạt phải xác thực email của họ." suspended_explanation: "Tài khoản tạm khóa không thể đăng nhập." block_explanation: "Tài khoản bị khóa không thể đăng bài hoặc tạo chủ đề." + stage_explanation: "Người dùng có cấp bậc chỉ có thể gửi bài qua email trong các chủ đề cụ thể." trust_level_change_failed: "Có lỗi xảy ra khi thay đổi mức độ tin tưởng của tài khoản." suspend_modal_title: "Tạm khóa Thành viên" + trust_level_2_users: "Độ tin cậy tài khoản mức 2" + trust_level_3_requirements: "Độ tin cậy bắt buộc mức 3" + trust_level_locked_tip: "mức độ tin cậy đang khóa, hệ thống sẽ không thể thăng hoặc giáng chức người dùng" + trust_level_unlocked_tip: "độ tin cậy đang được mở, hệ thống có thể thăng hoặc giáng chức người dùng" lock_trust_level: "Khóa Cấp độ Tin tưởng" + unlock_trust_level: "Mở khóa độ tin cậy" tl3_requirements: title: "Yêu cầu Cấp độ tin tưởng 3" + table_title: "Trong %{time_period} ngày qua:" value_heading: "Giá trị" requirement_heading: "Yêu cầu" visits: "Lượt xem" days: "ngày" + topics_replied_to: "Topics Replied To" topics_viewed: "Đã xem chủ đề" topics_viewed_all_time: "Đã xem chủ đề (mọi lúc)" posts_read: "Đọc bài viết" posts_read_all_time: "Đọc bài viết (mọi lúc)" flagged_posts: "Đã gắn cờ Bài viết" + flagged_by_users: "Users Who Flagged" + likes_given: "Lượt Likes" + likes_received: "Likes Đã Nhận" + likes_received_days: "Like nhận được: ngày độc nhất" + likes_received_users: "Like nhận được: tài khoản độc nhất" + qualifies: "Đủ điều kiện cho độ tin cậy mức 3." + does_not_qualify: "Không đủ điều kiện cho độ tin cậy mức 3." + will_be_promoted: "Sẽ sớm được thăng chức." + will_be_demoted: "Sẽ sớm bị giáng chức." + on_grace_period: "Hiện đang trong khoảng thời gian gia hạn thăng chức, sẽ không thể giáng chức." + locked_will_not_be_promoted: "Mức độ tin cậy đang khóa, sẽ không thể thăng chức." + locked_will_not_be_demoted: "Mức độ tin cậy đang khóa, sẽ không thể giáng chức." sso: title: "Single Sign On" external_id: "ID Bên ngoài" @@ -1855,6 +2269,9 @@ vi: external_email: "Email" external_avatar_url: "URL Ảnh đại diện" user_fields: + title: "Trường tài khoản" + help: "Thêm trường dữ liệu cho người dùng nhập." + create: "Tạo trường tài khoản" untitled: "Không có tiêu đề" name: "Tên Trường" type: "Loại Trường" @@ -1882,7 +2299,15 @@ vi: confirm: 'Xác nhận' dropdown: "Xổ xuống" site_text: + description: "Bạn có thể tùy chỉnh bất kỳ nội dung nào trên diễn đàn. Hãy bắt đầu bằng cách tìm kiếm dưới đây:" + search: "Tìm kiếm nội dung bạn muốn sửa" title: 'Nội Dung Chữ' + edit: 'sửa' + revert: "Hoàn nguyên thay đổi" + revert_confirm: "Bạn có chắc chắn muốn hoàn nguyên các thay đổi?" + go_back: "Quay lại tìm kiếm" + recommended: "Bạn nên tùy biến các nội dung sau đây cho phù hợp với nhu cầu:" + show_overriden: 'Chỉ hiển thị chỗ ghi đè' site_settings: show_overriden: 'Chỉ hiện thị đã ghi đè' title: 'Xác lập' @@ -1905,46 +2330,109 @@ vi: onebox: "Onebox" seo: 'SEO' spam: 'Rác' + rate_limits: 'Rate Limits' developer: 'Nhà phát triển' + embedding: "Embedding" + legal: "Legal" uncategorized: 'Khác' backups: "Sao lưu" login: "Đăng nhập" plugins: "Plugins" user_preferences: "Tùy chỉnh Tài khoản" badges: + title: Huy hiệu + new_badge: Thêm huy hiệu new: Mới name: Tên + badge: Huy hiệu display_name: Tên Hiển thị description: Mô tả + badge_type: Kiểu huy hiệu badge_grouping: Nhóm + badge_groupings: + modal_title: Nhóm huy hiệu + granted_by: Cấp bởi + granted_at: Cấp lúc reason_help: (Liên kết đến bài viết hoặc chủ đề) save: Lưu delete: Xóa + delete_confirm: Bạn có chắc chắn muốn xóa huy hiệu này? + revoke: Thu hồi reason: Lý do + expand: Mở rộng … + revoke_confirm: Bạn có chắc chắn muốn thu hồi huy hiệu này? + edit_badges: Sửa huy hiệu + grant_badge: Cấp huy hiệu + granted_badges: Cấp huy hiệu + grant: Cấp + no_user_badges: "%{name} chưa được cấp bất kỳ huy hiệu nào." + no_badges: Không có huy hiệu có thể được cấp. + none_selected: "Chọn một huy hiệu để bắt đầu" + allow_title: Cho phép huy hiệu được sử dụng như là tên + multiple_grant: Có thể được cấp nhiều lần + listable: Hiện huy hiệu trên trang huy hiệu công khai + enabled: Bật huy hiệu icon: Biểu tượng image: Hình ảnh + icon_help: "Sử dụng Font Awesome class hoặc URL của ảnh" + query: Truy vấn huy hiệu (SQL) + target_posts: Truy vấn bài viết mục tiêu + auto_revoke: Chạy truy vấn hủy bỏ hàng ngày + show_posts: Hiện bài viết được cấp huy hiệu trên trang huy hiệu + trigger: Phát động trigger_type: none: "Cập nhật hàng ngày" + post_revision: "Khi người dùng sửa hoặc tạo bài viết" + trust_level_change: "Khi người dùng thay đổi mức độ tin cậy" + user_change: "Khi người dùng được sửa hoặc được tạo" preview: + link_text: "Xem trước cấp huy hiệu" + plan_text: "Xem trước kế hoạch truy vấn" + modal_title: "Xem trước truy vấn huy hiệu" + sql_error_header: "Có lỗi xảy ra với truy vấn." + error_help: "Xem các liên kết sau đây để trợ giúp các truy vấn huy hiệu." bad_count_warning: header: "CẢNH BÁO!" + text: "Thiếu mẫu cấp độ huy hiệu, điều này xảy ra khi truy vấn huy hiệu trả về IDs tài khoản hoặc IDs bài viết không tồn tại. Điều này có thể gây ra kết quả bất ngờ sau này - hãy kiểm tra lại truy vấn của bạn lần nữa." + no_grant_count: "Không có huy hiệu nào được gán." + grant_count: + other: "%{count} huy hiệu đã được gán." sample: "Ví dụ:" grant: with: %{username} with_post: %{username} for post in %{link} + with_post_time: %{username} viết bài trong %{link} lúc %{time} + with_time: %{username} lúc %{time} emoji: + title: "Emoji" + help: "Thêm emoji mới có sẵn cho tất cả mọi người. (MẸO: kéo & thả nhiều file cùng lúc)" + add: "Thêm emoji mới" name: "Tên" image: "Hình ảnh" + delete_confirm: "Bạn có chắc chắn muốn xóa emoji :%{name}:?" embedding: + get_started: "Nếu bạn muốn nhúng Discourse trên một website khác, bắt đầu bằng cách thêm host." confirm_delete: "Bạn muốn xóa host này?" + sample: "Sử dụng mã HTML sau vào website để tạo và nhúng các chủ đề. Thay thế REPLACE_ME với Canonical URL của trang bạn muốn nhúng." + title: "Nhúng" host: "Cho phép Host" edit: "sửa" category: "Đăng vào Danh mục" add_host: "Thêm Host" + settings: "Thiết lập nhúng" feed_settings: "Cấu hình Feed" + feed_description: "Cung cấp RSS/ATOM cho website để cải thiện khả năng Discourse import nội dung của bạn." crawling_settings: "Cấu hình Crawler" + crawling_description: "Khi Discourse tạo chủ đề cho các bài viết của bạn, nếu không có RSS/ATOM thì hệ thống sẽ thử phân tích nội dung HTML. Đôi khi có thể gặp khó khăn khi trích xuất nội dung, vì vậy hệ thống cung cấp khả năng chỉ định quy tắc CSS để giúp quá trình trích xuất dễ dàng hơn." + embed_by_username: "Tên thành viên để tạo chủ đề" + embed_post_limit: "Số lượng tối đa bài viết được nhúng" + embed_username_key_from_feed: "Key to pull discourse username from feed" + embed_truncate: "Cắt ngắn các bài viết được nhúng" + embed_whitelist_selector: "Bộ chọn các thành phần CSS được hỗ trợ khi nhúng" embed_blacklist_selector: "CSS selector for elements that are removed from embeds" feed_polling_enabled: "Nhập bài viết bằng RSS/ATOM" + feed_polling_url: "URL của RSS/ATOM để thu thập" + save: "Lưu thiết lập nhúng" permalink: title: "Liên kết cố định" url: "URL" @@ -1955,6 +2443,7 @@ vi: category_id: "ID Danh mục" category_title: "Danh mục" external_url: "URL Bên ngoài" + delete_confirm: Bạn có chắc chắn muốn xóa liên kết tĩnh này? form: label: "Mới:" add: "Thêm" @@ -1980,35 +2469,54 @@ vi: title: 'Điều hướng' jump: '# Đến bài viết #' back: 'u Quay lại' + up_down: 'k/j Move selection ↑ ↓' open: 'o or Enter Mở chủ để đã chọn' next_prev: 'shift+j/shift+k Next/previous section' application: title: 'Ứng dụng' create: 'c Tạo mới chủ đề' notifications: 'n Mở thông báo' + hamburger_menu: '= Mở menu mobile' user_profile_menu: 'p Mở trình đơn thành viên' show_incoming_updated_topics: '. Show updated topics' search: '/ Tìm kiếm' + help: '? Mở trợ giúp bàn phím' dismiss_new_posts: 'x, r Dismiss New/Posts' dismiss_topics: 'x, t Bỏ qua bài viết' log_out: 'shift+z shift+z Đăng xuất' actions: title: 'Hành động' + bookmark_topic: 'f Chuyển chủ đề đánh dấu' pin_unpin_topic: 'shift+p Pin/Unpin bài viết' share_topic: 'shift+s Chia sẻ bài viết' share_post: 's Chia sẻ bài viết' reply_as_new_topic: 't Trả lời như là một liên kết đến bài viết' reply_topic: 'shift+r Trả lời bài viết' reply_post: 'r Trả lời bài viết' + quote_post: 'q Trích dẫn bài viết' like: 'l Thích bài viết' + flag: '! Đánh dấu bài viết' bookmark: 'b Đánh dấu bài viết' edit: 'e Sửa bài viết' delete: 'd Xóa bài viết' + mark_muted: 'm, m Mute topic' + mark_regular: 'm, r Chủ đề thông thường (mặc định)' + mark_tracking: 'm, t Theo dõi chủ đề' mark_watching: 'm, w theo dõi chủ đề' badges: + earned_n_times: + other: "Đạt được huy hiệu này %{count} lần" + more_with_badge: "Others with this badge" + title: Huy hiệu allow_title: "có thể sử dụng như là tiêu đề" + multiple_grant: "có thể trao tặng nhiều lần" + badge_count: + other: "%{count} Huy hiệu" more_badges: other: "+%{count} Thêm" + granted: + other: "%{count} được cấp" + select_badge_for_title: Chọn huy hiệu để sử dụng như là tên none: "" badge_grouping: getting_started: @@ -2027,32 +2535,51 @@ vi: description: Chỉnh sửa bàn viết lần đầu basic_user: name: Cơ bản + description: Các cấp chức năng cần thiết của cộng đồng member: name: Thành viên + description: Các cấp lời mời regular: name: Thường xuyên + description: Các cấp phân hạng, đổi tên và theo đuôi liên kết leader: name: Lãnh đạo + description: Các cấp sửa tổng thể, ghim, đóng, lưu trữ, tách và hợp nhất welcome: name: Chào mừng description: Đã nhận 1 lượt thích autobiographer: + name: Tự truyện description: Filled user profile information anniversary: name: Ngày kỷ niệm + description: Thành viên hoạt động trong năm, đăng bài ít nhất một lần + nice_post: + name: Bài viết hay + description: Được 10 like cho một bài viết, huy hiệu này có thể được cấp nhiều lần good_post: name: Bài viết tốt + description: Được 25 like cho một bài viết, huy hiệu này có thể được cấp nhiều lần great_post: name: Bài viết tuyệt vời + description: Được 50 like cho một bài viết, huy hiệu này có thể được cấp nhiều lần nice_topic: name: Bài viết hay + description: Được 10 like cho một chủ đề, huy hiệu này có thể được cấp nhiều lần good_topic: name: Chủ đề tốt + description: Được 25 like cho một chủ đề, huy hiệu này có thể được cấp nhiều lần + great_topic: + name: Chủ đề hay + description: Được 50 like cho một chủ đề, huy hiệu này có thể được cấp nhiều lần nice_share: + name: Nice Share description: Đã chia sẻ bài viết với 25 lượt người truy cập good_share: + name: Good Share description: Đã chia sẻ bài viết với 300 lượt người truy cập great_share: + name: Great Share description: Đã chia sẻ bài viết với 1000 lượt người truy cập first_like: name: Lượt thích đầu tiên @@ -2061,10 +2588,13 @@ vi: name: Đánh dấu đầu tiên description: Đánh dấu bài viết promoter: + name: Promoter description: Đã mời một thành viên campaigner: + name: Campaigner description: Mời 3 thành viên (Độ tin cậy 1) champion: + name: Champion description: Mời 5 thành viên (Độ tin cậy 2) first_share: name: Chia sẽ đầu tiên @@ -2077,13 +2607,25 @@ vi: description: Trích dẫn thành viên read_guidelines: name: Xem hướng dẫn + description: Xem nguyên tắc cộng đồng reader: name: Người xem description: Đọc tất cả bài viết trong các chủ để có hơn 100 bài popular_link: name: Liên kết phổ biến + description: Đăng một liên kết ngoài với ít nhất 50 click hot_link: name: Liên kết hấp dẫn + description: Đăng một liên kết ngoài với ít nhất 300 click famous_link: name: Liên kết phổ biến - + description: Đăng một liên kết ngoài với ít nhất 1000 click + google_search: | +

      Tìm kiếm với Google

      +

      +

      +

      diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index d34227a06d..e65ef2746d 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -81,6 +81,8 @@ zh_CN: other: "%{count}月后" x_years: other: "%{count}年后" + previous_month: '上个月' + next_month: '下个月' share: topic: '分享本主题的链接' post: '#%{postNumber} 楼' @@ -111,6 +113,19 @@ zh_CN: disabled: '于%{when}移除出列表' topic_admin_menu: "主题管理操作" emails_are_disabled: "所有的出站邮件已经被管理员全局禁用。将不发送任何邮件提醒。" + s3: + regions: + us_east_1: "美国东部(N. Virginia)" + us_west_1: "美国西部(N. California)" + us_west_2: "美国西部(Oregon)" + us_gov_west_1: "AWS GovCloud(美国)" + eu_west_1: "欧洲(Ireland)" + eu_central_1: "欧洲(Frankfurt)" + ap_southeast_1: "亚洲太平洋(Singapore)" + ap_southeast_2: "亚洲太平洋(Sydney)" + ap_northeast_1: "亚洲太平洋(Tokyo)" + ap_northeast_2: "亚洲太平洋(Seoul)" + sa_east_1: "南美(Sao Paulo)" edit: '编辑本主题的标题和分类' not_implemented: "非常抱歉,此功能暂时尚未实现!" no_value: "否" @@ -152,6 +167,7 @@ zh_CN: other: "%{count} 个字符" suggested_topics: title: "推荐主题" + pm_title: "推荐消息" about: simple_title: "关于" title: "关于%{title}" @@ -294,12 +310,16 @@ zh_CN: notifications: watching: title: "关注" + description: "你将会在该消息中的每个新帖子发布后收到通知,并且会显示新回复数量。" tracking: title: "追踪" + description: "你会在别人@你或回复你时收到通知,并且新帖数量也将在这些主题后显示。" regular: title: "普通" + description: "如果某人@你或者回复你,你将收到通知。" muted: title: "忽略" + description: "你不会收到组内关于新主题中的任何通知。" user_action_groups: '1': "给赞" '2': "被赞" @@ -379,6 +399,7 @@ zh_CN: not_supported: "通知功能暂不支持该浏览器。抱歉。" perm_default: "启用通知" perm_denied_btn: "拒绝授权" + perm_denied_expl: "你拒绝了通知提醒的权限。设置浏览器允许通知提醒。" disable: "禁用通知" currently_enabled: "(目前已启用)" enable: "启用通知" @@ -432,6 +453,8 @@ zh_CN: groups: "我的小组" bulk_select: "选择消息" move_to_inbox: "移动到收件箱" + move_to_archive: "存档" + failed_to_move: "移动选中消息失败(可能你的网络出问题了)" select_all: "全选" change_password: success: "(电子邮件已发送)" @@ -587,6 +610,21 @@ zh_CN: same_as_email: "你的密码与电子邮箱相同。" ok: "你设置的密码符合要求。" instructions: "至少需要 %{count} 个字符。" + summary: + title: "概要" + stats: "统计" + topic_count: "创建的主题" + post_count: "创建的帖子" + likes_given: "给出的赞" + likes_received: "收到的赞" + days_visited: "访问天数" + posts_read_count: "已读帖子" + top_replies: "热门回复" + top_topics: "热门帖子" + top_badges: "热门勋章" + more_topics: "更多主题" + more_replies: "更多回复" + more_badges: "更多徽章" associated_accounts: "登录" ip_address: title: "最后使用的 IP 地址" @@ -629,6 +667,7 @@ zh_CN: logout: "你已登出。" refresh: "刷新" read_only_mode: + enabled: "这个站点正处于只读模式。请继续浏览,但是回复、赞和其他操作暂时被禁用。" login_disabled: "只读模式下不允许登录。" too_few_topics_and_posts_notice: "让我们开始讨论!目前有 %{currentTopics} / %{requiredTopics} 个主题和 %{currentPosts} / %{requiredPosts} 个帖子。新访客需要能够阅读和回复一些讨论。" too_few_topics_notice: "让我们开始讨论!目前有 %{currentTopics} / %{requiredTopics} 个主题。新访客需要能够阅读和回复一些讨论。" @@ -742,6 +781,7 @@ zh_CN: ctrl: 'Ctrl' alt: 'Alt' composer: + emoji: "Emoji :)" more_emoji: "更多…" options: "选项" whisper: "密语" @@ -838,6 +878,8 @@ zh_CN: moved_post: "

      {{username}} 移动了 {{description}}

      " linked: "

      {{username}} {{description}}

      " granted_badge: "

      获得“{{description}}”

      " + group_message_summary: + other: "

      {{count}} 条消息在你的{{group_name}}组内的收件箱中

      " alt: mentioned: "被提及" quoted: "被引用" @@ -852,6 +894,7 @@ zh_CN: moved_post: "你的帖子被移动自" linked: "链接至你的帖子" granted_badge: "勋章授予" + group_message_summary: "在群组收件箱中的消息" popup: mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' group_mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' @@ -1003,6 +1046,7 @@ zh_CN: auto_close_title: '自动关闭设置' auto_close_save: "保存" auto_close_remove: "不要自动关闭该主题" + auto_close_immediate: "主题中的上个帖子是 %{hours} 小时前发出的,所以主题将会立即关闭。" progress: title: 主题进度 go_top: "顶部" @@ -1087,7 +1131,7 @@ zh_CN: help: '私下报告本帖以引起注意或者发送一条匿名通知' success_message: '你已成功报告本帖。' feature_topic: - title: "设为精华主题" + title: "聚焦该主题" pin: "将该主题置于{{categoryLink}}分类最上方至" confirm_pin: "你已经有了{{count}}个置顶主题。太多的置顶主题可能会困扰新用户和访客。你确定想要在该分类再置顶一个主题么?" unpin: "从{{categoryLink}}分类最上方移除主题。" @@ -1632,6 +1676,7 @@ zh_CN: refresh_report: "刷新报告" start_date: "开始日期" end_date: "结束日期" + groups: "所有群组" commits: latest_changes: "最近的更新:请经常升级!" by: "来自" @@ -1909,13 +1954,17 @@ zh_CN: name: '维基编辑' description: "维基帖子的背景颜色" email: + title: "邮件" settings: "设置" + templates: "模板" preview_digest: "预览" sending_test: "发送测试邮件..." error: "错误 - %{server_error}" test_error: "发送测试邮件时遇到问题。请再检查一遍邮件设置,确认你的主机没有封锁邮件链接,然后重试。" sent: "已发送" skipped: "跳过" + received: "收到" + rejected: "拒绝" sent_at: "发送时间" time: "时间" user: "用户" @@ -1933,6 +1982,19 @@ zh_CN: last_seen_user: "用户最后登录时间:" reply_key: "回复关键字" skipped_reason: "跳过理由" + incoming_emails: + from_address: "来自" + to_addresses: "发至" + cc_addresses: "抄送" + subject: "主题" + error: "错误" + none: "没有找到进站邮件。" + filters: + from_placeholder: "from@example.com" + to_placeholder: "to@example.com" + cc_placeholder: "cc@example.com" + subject_placeholder: "主题..." + error_placeholder: "错误" logs: none: "未发现日志。" filters: @@ -1996,6 +2058,12 @@ zh_CN: change_category_settings: "更改分类设置" delete_category: "删除分类" create_category: "创建分类" + block_user: "封禁用户" + unblock_user: "解封用户" + grant_admin: "授予管理员权限" + revoke_admin: "撤销管理员权限" + grant_moderation: "授予版主权限" + revoke_moderation: "撤销版主权限" screened_emails: title: "被屏蔽的邮件地址" description: "当有人试图用以下邮件地址注册时,将受到阻止或其它系统操作。" @@ -2093,6 +2161,7 @@ zh_CN: moderator: "版主?" admin: "管理员?" blocked: "已封?" + staged: "部分权限?" show_admin_profile: "管理员" edit_title: "编辑头衔" save_title: "保存头衔" @@ -2154,9 +2223,12 @@ zh_CN: deactivate_failed: "在停用用户帐号时发生了错误。" unblock_failed: '在解除用户帐号封禁时发生了错误。' block_failed: '在封禁用户帐号时发生了错误。' + block_confirm: '你确定要封禁用户吗?他们将没有办法创建任何主题或者帖子。' + block_accept: '是的,封禁用户' deactivate_explanation: "已停用的用户必须重新验证他们的电子邮件。" suspended_explanation: "一个被封禁的用户不能登录。" block_explanation: "被封禁的用户不能发表主题或者评论。" + stage_explanation: "部分访问权限的用户只能通过邮件在特定主题内发表帖子。" trust_level_change_failed: "改变用户等级时出现了一个问题。" suspend_modal_title: "被禁用户" trust_level_2_users: "二级信任等级用户" @@ -2434,6 +2506,9 @@ zh_CN: mark_tracking: 'm 然后 t 追踪主题' mark_watching: 'm 然后 w 关注主题' badges: + earned_n_times: + other: "授予徽章 %{count} 次" + more_with_badge: "其他有这些徽章的人" title: 徽章 allow_title: "能用作头衔" multiple_grant: "能被授予多次" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index a63a9a598e..f6d09f6179 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -27,6 +27,9 @@ ar: purge_reason: "حُذف آليا باعتباره حسابا إما مهجورا أو غير منشّطا" disable_remote_images_download_reason: "عُطّل تنزيل الصور عن بعد بسبب نفاذ مساحة القرص الحرّة." anonymous: "مجهول" + emails: + incoming: + default_subject: "رسالة ورادة من %{email}" errors: &errors format: '%{attribute} %{message}' messages: @@ -1110,6 +1113,7 @@ ar: strip_images_from_short_emails: "شريط الصور من البريد الإلكتروني لها حجم أقل من 2800 بايت" short_email_length: "طول أقصر بريد الكتروني بـ Bytes." display_name_on_email_from: "أعرض الاسماء كاملة في البريد الالكتروني من المجال." + delete_email_logs_after_days: "حذف سجل البريد بعد (ن) يوم. 0 لحفظ السجل للابد" pop3_polling_enabled: "تصويت عبرPOP3 لردود البريد الإلكتروني." pop3_polling_ssl: "أستخدم SSL عند الأتصال بمخدم POP3.(مُستحسن)" pop3_polling_period_mins: "الفترة دقائق بين التحقق من حساب POP3 للبريد الإلكتروني. ملاحظة : تتطلب إعادة تشغيل." @@ -1758,6 +1762,16 @@ ar: --- %{respond_instructions} + user_replied_pm: + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + --- + %{respond_instructions} user_quoted: subject_template: "[%{site_name}] %{topic_title}" text_body_template: | diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 66051a31ff..34bef2d429 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -27,6 +27,9 @@ de: purge_reason: "Stillgelegtes, nicht aktives Konto wurde automatisch gelöscht." disable_remote_images_download_reason: "Der Download von Bildern wurde deaktiviert, weil nicht mehr genug Plattenplatz vorhanden war." anonymous: "Anonym" + emails: + incoming: + default_subject: "Eingehende E-Mail von %{email}" errors: &errors format: '%{attribute} %{message}' messages: @@ -288,6 +291,7 @@ de: one: "Diese Kategorie kann nicht gelöscht werden, weil sie ein Thema enthält. Das Thema ist %{topic_link}." other: "Diese Kategorie kann nicht gelöscht werden, weil sie %{count} Themen enthält. Das älteste Thema ist %{topic_link}." topic_exists_no_oldest: "Diese Kategorie kann nicht gelöscht werden, weil sie #{category.topic_count} Themen enthält." + uncategorized_description: "Themen welche keine Kategorie benötigen oder in keine existierende Kategorie passen." trust_levels: newuser: title: "Neuer Benutzer" @@ -822,6 +826,7 @@ de: active_user_rate_limit_secs: "Sekunden, nach denen das 'last_seen_at'-Feld aktualisiert wird." verbose_localization: "Erweiterte Lokalisierungstipps in der Benutzeroberfläche anzeigen " previous_visit_timeout_hours: "Stunden, die ein Besuch dauert, bevor er als 'früherer' Besuch gezählt wird." + top_topics_formula_log_views_multiplier: "Wert von Log Ansichten Multiplikator (n) in Top Themen Formel: `log(views_count) * (n) + op_likes_count * 0.5 + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`" rate_limit_create_topic: "Nach Erstellen eines Themas muss ein Nutzer (n) Sekunden warten, bevor ein weiteres Thema erstellt werden kann." rate_limit_create_post: "Nach Schreiben eines Beitrags muss ein Nutzer (n) Sekunden warten, bevor ein weiterer Beitrag erstellt werden kann." rate_limit_new_user_create_topic: "Nach Erstellen eines Themas muss ein neuer Nutzer (n) Sekunden warten, bevor ein weiteres Thema erstellt werden kann." @@ -865,6 +870,7 @@ de: tl2_requires_likes_given: "Die Anzahl der Likes, die ein Benutzer vergeben muss, bevor er in die Vertrauensstufe 2 befördert wird." tl2_requires_topic_reply_count: "Die Anzahl der Beiträge, auf die ein Benutzer antworten muss, bevor er in die Vertrauensstufe 2 befördert wird." tl3_time_period: "Zeitraum für die Bedingungen für Vertrauensstufe 3" + tl3_requires_days_visited: "Minimale Anzahl an Tagen, an denen der Nutzer die Seite besuchen haben muss, in den letzten 100 Tagen, um sich für eine Beförderung zu Vertrauenslevel 3 zu qualifizieren. Setze tl3 höher als die Zeitperiode um Beförderungen zu Vertrauenslevel 3 zu deaktivieren. (0 oder höher)" tl3_requires_topics_replied_to: "Die Mindestanzahl an Themen, auf die ein Benutzer in den letzten 100 Tagen geantwortet haben muss, um die Vertrauensstufe 3 erreichen zu können. (0 oder mehr)" tl3_requires_topics_viewed: "Prozentualer Anteil aller Themen der letzten 100 Tage, die ein Nutzer mindestens gelesen haben muss, um die Vertrauensstufe Anführer (3) erreichen zu können. (0 bis 100)" tl3_requires_posts_read: "Prozentualer Anteil aller Beiträge der letzten 100 Tage, die ein Nutzer mindestens gelesen haben muss, um die Vertrauensstufe Anführer (3) erreichen zu können. (0 bis 100)" @@ -877,6 +883,7 @@ de: tl3_links_no_follow: "rel=nofollow nicht von Links entfernen, die von Benutzern mit Vertrauensstufe 3 erstellt wurden." min_trust_to_create_topic: "Die minimale Vertrauensstufe wird benötigt um eine neues Thema zu erstellen." min_trust_to_edit_wiki_post: "Die minimal benötigte Vertrauensstufe, um als Wiki markierte Beiträge bearbeiten zu können." + min_trust_to_allow_self_wiki: "Das mindeste Vertrauenslevel, das ein Nutzer haben muss, um einen eigenen Wiki Eintrag zu erstellen." min_trust_to_send_messages: "Benötigte Vertrauensstufe um neue private Nachrichten erstellen zu dürfen." newuser_max_links: "Maximale Anzahl der Links, die neue Benutzer Beiträgen hinzufügen dürfen." newuser_max_images: "Maximale Anzahl der Bilder, die neue Benutzer Beiträgen hinzufügen dürfen." @@ -887,6 +894,7 @@ de: max_users_notified_per_group_mention: "Maximale Anzahl an Nutzern die benachrichtigt werden wenn eine Gruppe erwähnt wird (wird die Grenze erreicht, werden keine Nutzer benachrichtigt)" create_thumbnails: "Erzeuge ein Vorschaubild und eine Lightbox für Bilder, die zu groß sind, um in einem Beitrag angezeigt zu werden." email_time_window_mins: "Warte (n) Minuten, bevor Nutzern eine Hinweis-E-Mail geschickt wird, um ihnen Gelegenheit zu geben, ihre Beiträge abschließend bearbeiten zu können." + private_email_time_window_seconds: "Warte (n) Sekunden, bevor irgendwelche private E-Mail Benachrichtigungen gesendet werden, um den Nutzer die Möglichkeit zu geben, den Beitrag zu bearbeiten und finalisieren." email_posts_context: "Anzahl der Antworten welche als Konext einer Notifikations-Mail hinzugefügt werden." flush_timings_secs: "Sekunden, nach denen Zeiteinstellungen auf den Server übertragen werden." title_max_word_length: "Maximal erlaubte Wortlänge in Thementiteln, in Zeichen." @@ -943,6 +951,7 @@ de: display_name_on_email_from: "Zeige vollständige Namen im Absender-Feld von E-Mails" unsubscribe_via_email: "Erlaube es Benutzern eine E-Mail mit dem Betreff oder Text: \"unsubscribe\" zum abbestellen der E-Mails zu senden." unsubscribe_via_email_footer: "Hänge einen Link zum abbestellen ans Ende der E-Mail." + delete_email_logs_after_days: "Lösche E-Mail Logs nach (N) Tagen. 0 um sie für immer zu behalten." pop3_polling_enabled: "E-Mail-Antworten über POP3 abholen." pop3_polling_ssl: "SSL für die Verbindung zum POP3-Server verwenden. (Empfohlen)" pop3_polling_period_mins: "Intervall in Minuten zum Abholen neuer E-Mails vom POP3-Konto. HINWEIS: benötigt Neustart." @@ -969,7 +978,7 @@ de: automatically_download_gravatars: "Avatare von Gravatar herunterladen, wenn ein Nutzer sich registriert oder seine E-Mail-Adresse ändert." digest_topics: "Maximale Anzahl von Themen, die in der E-Mail-Zusammenfassung angezeigt werden." digest_min_excerpt_length: "Minimale Länge des Auszugs aus einem Beitrag in der E-Mail-Zusammenfassung, in Zeichen." - delete_digest_email_after_days: "Sende keine E-Mail-Zusammenfassungen an Benutzer, die die Seite seit mehr als (n) Tagen nicht mehr besucht haben." + delete_digest_email_after_days: "Keine Zusammenfassungen an Nutzer senden, welche für mehr als (n) Tage nicht auf der Seite aktiv waren." disable_digest_emails: "E-Mail-Zusammenfassungen für alle Benutzer deaktivieren." detect_custom_avatars: "Aktiviere diese Option, um zu überprüfen, ob Benutzer eigene Profilbilder hochgeladen haben." max_daily_gravatar_crawls: "Wie oft pro Tag Discourse höchstens auf Gravatar nach benuterdefinierten Avataren suchen soll." @@ -981,6 +990,7 @@ de: anonymous_account_duration_minutes: "Um die Anonymität der virtuellen anonymen Nutzer zu erhalten, erzeuge ein neues anonymes Konto alle N Minuten je Benutzer. Beispiel: wenn dies auf 600 gesetzt ist wird ein neues anonymes Konto erzeugt, wenn ein Benutzer in den anonymen Modus wechselt UND mindestens 600 Minuten seit der letzten anonymen Nachricht dieses Nutzers vergangen sind." hide_user_profiles_from_public: "Deaktiviert Benutzerkarten, Nutzerprofile und das Benutzerverzeichnis für anonyme Nutzer." allow_profile_backgrounds: "Erlaube Benutzern Profilhintergründe hochzuladen." + sequential_replies_threshold: "Anzahl an Beiträgen die ein Nutzer machen muss, um benachrichtigt zu werden, dass er zu viele aufeinanderfolgende Antworten schreibt." enable_mobile_theme: "Mobilgeräte verwenden eine mobile Darstellung mit der Möglichkeit zur vollständigen Seite zu wechseln. Deaktiviere diese Option, wenn du ein eigenes Full-Responsive-Stylesheet verwenden möchtest." dominating_topic_minimum_percent: "Anteil der Nachrichten eines Themas in Prozent, die ein einzelner Nutzer verfassen darf, bevor dieser Nutzer darauf hingewiesen wird, dass er dieses Thema dominiert." disable_avatar_education_message: "Weise Benutzer nicht darauf hin, dass sie ihren Avatar ändern können" @@ -990,6 +1000,7 @@ de: global_notice: "Zeigt allen Besuchern eine DRINGENDE NOTFALL-NACHRICHT als global sichtbares Banner an. Deaktiviert bei leerer Nachricht. (HTML ist erlaubt.)" disable_edit_notifications: "Unterdrückt Bearbeitungshinweise durch den System-Nutzer, wenn die 'download_remote_images_to_local' Einstellung aktiviert ist." automatically_unpin_topics: "Themen automatisch loslösen, wenn ein Nutzer das Ende erreicht." + read_time_word_count: "Wörteranzahl pro Minute um die abgeschätzte Lesezeit zu berechnen." full_name_required: "Der voller Name wird für das Benutzerprofil benötigt." enable_names: "Zeigt den vollen Namen eines Benutzers auf dem Profil, der Benutzerkarte und in E-Mails an. Wenn deaktiviert wird der volle Name überall ausgeblendet." display_name_on_posts: "Zeige zusätzlich zum @Benutzernamen auch den vollen Namen des Benutzers bei seinen Beiträgen." @@ -1013,6 +1024,7 @@ de: enable_cdn_js_debugging: "Ermöglicht die Anzeige vollständiger Fehler auf /logs, indem alle eingebetteten JavaScripts Cross-Origin Zugriffsberechtigungen erhalten." show_create_topics_notice: "Administratoren eine Warnmeldung anzeigen, wenn im Forum weniger als 5 öffentlich sichtbare Themen existieren." delete_drafts_older_than_n_days: Lösche Entwürfe, die mehr als (n) Tage alt sind. + vacuum_db_days: "Führe VACUUM ANALYZE aus, um Datenbankspeicher nach Migrationen zurückzuerhalten (0 um zu deaktivieren)" prevent_anons_from_downloading_files: "Nichtangemeldeten Benutzern das Herunterladen von Anhängen verbieten. WARNUNG: dies verhindert auch das Herunterladen jeglicher Ressourcen für Website-Anpassungen, die als Anhänge gespeichert wurden." slug_generation_method: "Gib eine Methode an, wie Kürzel in URLs generiert werden sollen. 'encoded' verwendet einen Prozent-Encodierten String, bei 'none' wird kein Kürzel verwendet." enable_emoji: "Aktiviere emoji" @@ -1051,7 +1063,12 @@ de: invalid_string_min: "Muss mindestens %{min} Zeichen lang sein." invalid_string_max: "Darf nicht länger als %{max} Zeichen sein." invalid_reply_by_email_address: "Adresse muss '%{reply_key}' enthalten und sich von der Benachrichtigungs E-Mail-Adresse unterscheiden." + pop3_polling_host_is_empty: "Du musst einen 'POP3 Abfrage Host' definieren, um POP3 Abfragen zu aktivieren." + pop3_polling_username_is_empty: "Du musst einen 'POP3 Abfrage Nutzername' definieren, um POP3 Abfragen zu aktivieren." + pop3_polling_password_is_empty: "Du must ein 'POP3 Abfrage Passwort' definieren, um POP3 Abfragen zu aktivieren." + pop3_polling_authentication_failed: "POP3 Authentifizierung ist fehlgeschlagen. Bitte überprüfe die POP3 Anmeldedaten." notification_types: + group_mentioned: "%{group_name} wurde auf %{link} erwähnt" mentioned: "%{display_username} hat Dich in %{link} erwähnt." liked: "%{display_username} gefällt deinen Beitrag in %{link}." replied: "%{display_username} hat auf deinen Beitrag in %{link} geantwortet." @@ -1161,12 +1178,23 @@ de: must_begin_with_alphanumeric: "muss mit einem Buchstaben, einer Zahl oder einem Unterstrich beginnen" must_end_with_alphanumeric: "muss mit einem Buchstaben, einer Zahl oder einem Unterstrich enden" must_not_contain_two_special_chars_in_seq: "muss keine Reihenfolge von 2 oder mehr Sonderzeichen (.-_) haben" + must_not_end_with_confusing_suffix: "Darf nicht mit einem Suffix wie json, png, etc. enden." email: not_allowed: "ist für diesen Mailprovider nicht erlaubt. Bitte verwende eine andere Mailadresse." blocked: "ist nicht erlaubt." ip_address: blocked: "Von Deiner IP Adresse aus ist es nicht erlaub sich zu registrieren." max_new_accounts_per_registration_ip: "Weitere Registrierungen sind von deiner IP-Adresse aus nicht gestattet (limit erreicht). Kontaktiere einen Administrator mit dem Problem damit er dir helfen kann." + unsubscribe_mailer: + subject_template: "Bestätige, dass du nicht länger E-Mail Updates von %{site_title} erhalten möchtest" + text_body_template: | + Jemand (wahrscheinlich du?) hat angefragt, nicht länger E-Mail Updates von %{site_domain_name} auf dieser Adresse zu erhalten. + Wenn du nicht länger Benachrichtigungen erhalten möchtest, dann klicke bitte diesen Link: + + %{confirm_unsubscribe_link} + + + Wenn du weiterhin E-Mail Updates erhalten möchtest, dann kannst du diese E-Mail ignorieren. invite_mailer: subject_template: "%{invitee_name} hat dich zum Thema '%{topic_title}' auf %{site_domain_name} eingeladen" text_body_template: | @@ -1411,12 +1439,22 @@ de: csv_export_failed: subject_template: "Datenexport fehlgeschlagen" text_body_template: "Entschuldigung, beim Exportieren deiner Daten trat ein Fehler auf. Bitte kontaktiere einen Mitarbeiter oder sieh in die Logs." + email_reject_insufficient_trust_level: + subject_template: "[%{site_name}] E-Mail Problem -- Unzureichendes Vertrauenslevel" + text_body_template: | + Es tut uns leid, aber das Senden deiner E-Mail Nachricht an %{destination} (betitelt mit %{former_title}) hat nicht funktioniert. + + Dein Benutzerkonto hat nicht das benötigte Vertrauenslevel um neue Themen an diese E-Mail Adresse zu senden. Wenn du glaubst, dass dies ein Fehler ist, dann kontaktiere bitte einen Mitarbeiter. + email_reject_inactive_user: + subject_template: "[%{site_name}] E-Mail Problem -- Inaktiver Nutzer" + text_body_template: | + Es tut uns leid, aber das Senden deiner E-Mail Nachricht an %{destination} (betitelt mit %{former_title}) hat nicht funktioniert. + + Dein Benutzerkonto, assoziiert mit dieser E-Mail Adresse ist nicht aktiviert. Bitte aktiviere dein Konto bevor du E-Mails sendest. + email_reject_reply_user_not_matching: + subject_template: "[%{site_name}] E-Mail Problem -- Antwort Nutzer stimmt nicht überein" email_reject_no_account: subject_template: "[%{site_name}] E-Mail-Problem -- Unbekanntes Konto" - text_body_template: | - Es tut uns leid, aber deine E-Mail-Nachricht an %{destination} (Titel: %{former_title}) konnte nicht verarbeitet werden! - - Es ist kein Konto mit dieser E-Mail-Adresse bekannt. Versuche die Nachricht von einer anderen, im Forum registrierten E-Mail-Adresse zu verschicken oder kontaktiere einen Mitarbeiter. email_reject_empty: subject_template: "[%{site_name}] E-Mail-Problem -- Kein Inhalt" text_body_template: | diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index e4cd55c408..22dc369c50 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -904,6 +904,7 @@ es: max_users_notified_per_group_mention: "Número de usuarios máximos que serán notificados si un grupo es mencionado (si se llega al límite no se mandarán más invitaciones)" create_thumbnails: "Crear miniaturas de imágenes y lightbox cuando estas son demasiado grandes para encajar en un post." email_time_window_mins: "Esperar (n) minutos antes de enviar cualquier email de notificación, para dar a los usuarios margen con el que editar y finalizar sus posts." + private_email_time_window_seconds: "Espera (n) segundos antes de enviar emails de notificación privada, para dar tiempo a los usuarios a editar y finalizar sus posts." email_posts_context: "Cuántas respuestas previas se incluirán como contexto en los emails de notificación." flush_timings_secs: "Cuán frecuente, en segundos, se alinean los datos de sincronización con el servidor." title_max_word_length: "La longitud máxima permitida de una palabra, en caracteres, en el título del tema." @@ -960,6 +961,7 @@ es: display_name_on_email_from: "Mostrar nombres completos en los campos de remitente de emails" unsubscribe_via_email: "Permitir a los usuarios darse de baja de los emails respondiendo con el texto 'unsubscribe' en el asunto o el cuerpo del mensaje" unsubscribe_via_email_footer: "Adjuntar un enlace para darse de baja al pie de los emails enviados" + delete_email_logs_after_days: "Eliminar logs de email después de (N) días. Si es 0 permanecerán de forma indefinida." pop3_polling_enabled: "Poll vía POP3 para respuestas de e-mail." pop3_polling_ssl: "Usar SSL mientras se conecta al servidor POP3. (Recomendado)" pop3_polling_period_mins: "El período en minutos entre revisiones de correo de la cuenta POP3. NOTA: requiere reiniciar." @@ -986,7 +988,7 @@ es: automatically_download_gravatars: "Descargar Gravatars para usuarios cuando se creen una cuenta o cambien el email." digest_topics: "El número máximo de temas a mostrar en el resumen por email." digest_min_excerpt_length: "La extensión mínima, en caracteres, del extracto de un post en el resumen por email." - delete_digest_email_after_days: "Suprimir los emails de resumen para aquellos usuarios que no han visto el sitio desde más de (n) días." + delete_digest_email_after_days: "Omitir los emails de resumen para usuarios que no hayan visto el sitio después de (n) días." disable_digest_emails: "Inhabilitar e-mails de resumen para todos los usuarios." detect_custom_avatars: "Verificar o no que los usuarios han subido una imagen de perfil." max_daily_gravatar_crawls: "Máximo número de veces que Discourse comprobará Gravatar en busca de avatares personalizados en un día" @@ -1353,6 +1355,8 @@ es: Sin embargo, si el post fuera ocultado por la comunidad una segunda vez, permanecerá oculto hasta que sea revisado por un moderador – y en ese caso puede que se tomen medidas, incluyendo la posible suspesión de tu cuenta. Para más información, por favor consulta nuestras [directrices](%{base_url}/guidelines). + usage_tips: + text_body_template: "Aquí tienes unos breves consejos para empezar:\n\n## Lectura\n\nPara leer más, **¡tan solo tienes que hacer scroll!**\n\nTan pronto como se publiquen nuevas respuestas o temas, aparecerán automáticamente – sin necesidad de actualizar la página.\n\n## Navegación\n\n- Para realizar una búsqueda, ver tu perfil de usuario, o el menú , usa los **botones de la esquina superior derecha**.\n\n- Entrar a un tema desde su título te llevará a la **última respuesta sin leer** del hilo. Para ir al primer o al último post, haz clic en el contador de respuestas o en la fecha de última respuesta..\n\n \n\n- Mientras lees un tema, selecciona la barra de progreso en la esquina inferior derecha para ver los controles de navegación. Podrás ir al inicio del hilo seleccionando su título. Teclea ? para ver una lista de rápidos atajos de teclado.\n\n \n\n## Participación\n\n- Para responder al **tema en general**, utiliza al final del hilo.\n\n- Para responder a **una persona en específico**, utiliza en su post.\n\n- Para responder **creando un nuevo tema**, utiliza la derecha del post. Tanto el tema original como el nuevo serán enlazados entre sí automáticamente.\n\nTu respuesta puede estar en varios formatos como HTML simple, BBCode, o [Markdown](http://commonmark.org/help/):\n\n Esto es **negrita**.\n Esto es negrita.\n Esto es [b]negrita[/b].\n\n¿Quieres aprender Markdown? [¡Echa un vistazo a nuestro divertido tutorial interactivo de 10 minutos!](http://commonmark.org/help/tutorial/)\n\nPara escribir una cita, selecciona el texto que quieres citar, después haz clic en el botón citar. ¡Puedes repetir para realizar múltiples citas!\n\n\n\nPara notificar a alguien en tu respuesta, menciónale. Empieza a escribir `@` para elegir su nombre de usuario.\n\n\n\nPara usar [Emojis](http://www.emoji.codes/), teclea `:` para elegir por nombre, o usa los tradicionales smileys `;)`\n\n\n\nPara generar un extracto desde un enlace, pégalo en una línea aparte para él solo:\n\n\n\n## Acciones\n\nHay algunos botones de acción al final de cada post:\n\n\n\nPara hacerle saber a alguien que aprecias su post, utiliza el botón de **Me gusta**. ¡Compartir es amor!\n\nSi ves algún problema en la publicación de alguien, escríbele un privado, o házselo saber [a nuestros moderadores](%{base_url}/about), con el botón **reporte**. También puedes **compartir** un enlace a un post, o guardarlo en **marcadores** para tenerlo de referencia en tu página de perfil.\n\n## Notificaciones\n\nCuando alguien te responde, cita tu post, o bien menciona tu `@usuario`, inmediatamente aparecerá un número en la esquina superior derecha de la página. Usa ese botón para acceder a tus **notificaciones**.\n\n\n\nNo te preocupes si te pierdes alguna respuesta – te enviaremos un email con las notificaciones que lleguen cuando estés fuera.\n\n## Tus preferencias\n\n - Todos los temas creados hace menos de **dos días** se consideran nuevos..\n\n - Aquellos temas en los que hayas **participado activamente** (porque lo hayas creado, o respondido o leído durante un período razonable) serán seguidos por tu usuario automáticamente. \n\nVerás los indicadores de temas nuevos o número de respuestas no leídas al lado de estos temas::\n\n\n\nPuedes cambiar tus notificaciones para cualquier tema mediante el control de notificaciones al final del tema.\n\n\n\nTambién puedes establecer el estado de notificación de una categoría entera, si quieres vigilar cualquier nuevo tema en una categoría específica.\n\nPara cambiar cualquiera de estas opciones, puedes ver [tus preferencias](%{base_url}/my/preferences).\n\n## Confianza y comunidad\n\nAl participar aquí, con el tiempo te ganarás la confianza de la comunidad, te convertirás en usuario de pleno derecho y te serán retiradas las limitaciones de nuevo usuario. Llegado a un punto de un alto [nivel de confianza](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924), obtendrás además nuevas habilidades para ayudarnos a gestionar la comunidad juntos.\n" welcome_user: subject_template: "¡Bienvenido a %{site_name}!" text_body_template: | @@ -1575,8 +1579,10 @@ es: unsubscribe: title: "Darse de baja" description: "¿No estás interesado en recibir estos emails? ¡No hay problema! Haz clic abajo para darte de baja de forma instantánea:" - reply_by_email: "[Visita el tema](%{base_url}%{url}) o responde a este email para publicar." - visit_link_to_respond: "[Visita el tema](%{base_url}%{url}) para responder." + reply_by_email: "[Visita el tema](%{base_url}%{url}) o responde a este email para comentar" + visit_link_to_respond: "[Visita el tema](%{base_url}%{url}) para comentar" + reply_by_email_pm: "[Visita el mensaje](%{base_url}%{url}) o responde a este email para comentar" + visit_link_to_respond_pm: "[Visita el mensaje](%{base_url}%{url}) para comentar" posted_by: "Publicado por %{username} el %{post_date}" user_invited_to_private_message_pm: subject_template: "[%{site_name}] %{username} te ha invitado a un hilo de mensajes '%{topic_title}'" diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 48c387f366..ee1b40ac85 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -915,6 +915,7 @@ fi: max_users_notified_per_group_mention: "Kuinka moni voi saada ilmoituksen, kun ryhmään viitataan (jos raja ylittyy, kukaan ei saa ilmoitusta)" create_thumbnails: "Luo esikatselu- ja lightbox-kuvia, jotka ovat liian suuria mahtuakseen viestiin." email_time_window_mins: "Odota (n) minuuttia ennen ilmoitussähköpostien lähettämistä, jotta käyttäjällä on aikaa muokata ja viimeistellä viestinsä." + private_email_time_window_seconds: "Odota (n) sekuntia ennen ilmoitusten lähettämistä käyttäjille, jotta kirjoittajalla on mahdollisuus muokata ja viimeistellä viestinsä." email_posts_context: "Kuinka monta edellistä vastausta liitetään kontekstiksi sähköposti-ilmoituksessa." flush_timings_secs: "Kuinka usein timing data päivitetään palvelimelle, sekunneissa." title_max_word_length: "Sanan enimmäispituus merkkeinä ketjun otsikossa" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index d07d23a93b..a86d2f685e 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -1060,6 +1060,7 @@ fr: invalid_string_min: "Doit être d'au moins %{count} caractères." invalid_string_max: "Ne doit pas être supérieur à %{max} caractères." invalid_reply_by_email_address: "La valeur doit contenir '%{reply_key}' et être différente de la notification email." + pop3_polling_authentication_failed: "Echec d'authentication POP3. Veuillez contrôler vos détails POP3." notification_types: group_mentioned: "%{group_name} a été mentionné dans %{link}" mentioned: "%{display_username} vous a mentionné dans %{link}" @@ -1171,12 +1172,23 @@ fr: must_begin_with_alphanumeric: "doit débuter par une lettre ou un nombre ou un underscore" must_end_with_alphanumeric: "doit terminer par une lettre, un chiffre ou un caractère souligné (underscore)" must_not_contain_two_special_chars_in_seq: "ne doit pas contenir une séquence de 2 caractères spéciaux ou plus (.-_)" + must_not_end_with_confusing_suffix: "ne doit pas se terminer avec un suffixe déroutant comme .json ou .png etc." email: not_allowed: "n'est pas autorisé pour ce fournisseur de courriels. Merci d'utiliser une autre adresse." blocked: "n'est pas autorisé." ip_address: blocked: "Les nouvelles inscriptions ne sont pas acceptées depuis votre adresse IP." max_new_accounts_per_registration_ip: "Les nouvelles inscriptions ne sont pas autorisées depuis votre adresse IP (limite atteinte). Contactez un responsable." + unsubscribe_mailer: + subject_template: "Confirmez que vous ne souhaitez plus recevoir d'email de mise à jour de %{site_title}" + text_body_template: | + Quelqu’un (est-ce vous?) a demandé à ne plus envoyer d'email de mise à jour de %{site_domain_name} a cette adresse. + Pour confirmer, merci de cliquer sur ce lien: + + %{confirm_unsubscribe_link} + + + Si vous souhaitez continuer à recevoir les emails, vous pouvez ignorer cette email. invite_mailer: subject_template: "%{invitee_name} vous a invité(e) à participer à '%{topic_title}' sur %{site_domain_name}" text_body_template: | @@ -1339,6 +1351,103 @@ fr: Afin d'être guider, merci de vous référer à notre [charte](%{base_url}/guidelines). + usage_tips: + text_body_template: | + Vous trouverez ci-dessous quelques astuces pour vous aider à démarrer : + + ## Lire + + Pour en lire plus, **faites défiler vers le bas !** + + Lorsque de nouveaux sujets ou de nouvelles réponses sont publiés, ils apparaissent automatiquement ; inutile de rafraîchir la page. + + ## Navigation + + - Pour effectuer une recherche, accéder à votre profil ou au utiliser le menu , cliquez sur les icônes en haut à droite. + + - Sélectionner le titre d'un article, vous dirigera toujours vers **la nouvelle réponse non lue** dans celui-ci. Pour aller au début ou à la fin, sélectionnez le compteur de réponses ou la date de la dernière réponse. + + + + - Pendant la lecture d'un article, sélectionnez la barre de progression en bas à droite pour utiliser les commandes de navigation. Revenez directement au début en cliquant sur le titre de l'article. Appuyez sur ? pour afficher la liste des raccourcis clavier. + + + + ## Répondre + + - Pour répondre **de manière générale à une conversation**, utilisez tout en bas de celle-ci. + + - Pour répondre à **une personne en particulier**, utilisez sur leur message. + + - Pour répondre en initiant **une nouvelle conversation**, utilisez qui se trouve à la droite du message. L'ancien et le nouveau sujet seront alors automatiquement liés. + + Votre réponse peut être mise page en utilisant soit du HTML simple, soit les syntaxes BBCode ou [Markdown](http://commonmark.org/help/) : + + Ceci est **gras**. + Ceci est gras. + Ceci est [b]gras<[/b]. + + Vous voulez apprendre Markdown ? [Suivez notre amusant tutoriel interactif de 10 minutes !](http://commonmark.org/help/tutorial/) + + Pour insérer une citation, sélectionnez le texte que vous souhaitez citer, puis cliquez sur n'importe quel bouton Répondre. Répétez l'opération pour insérer d'autres citations. + + + + Pour signaler à quelqu'un votre réponse, mentionnez son nom. Tapez un `@` pour commencer à choisir un utilisateur. + + + + Pour insérer un [émoticône standard Emoji](http://www.emoji.codes/), utilisez le caractère `:` et tapez son nom ou utilisez des des émoticônes traditionnels comme `;)`. + + + + Pour générer l'aperçu d'un lien, mettez le lien seul sur une ligne. + + + + ## Actions + + Il y a des boutons d'actions en bas de chaque commentaire : + + + + Pour prévenir quelqu'un que vous appréciez son commentaire, cliquez **sur le cœur**. Partagez votre amour ! + + Si un commentaire vous pose problème, prévenez soit son auteur en privé, soit [l'équipe de modération](%{base_url}/about) grâce au bouton **drapeau**. + + Vous pouvez aussi partager un lien vers une contribution ou lui mettre un signet afin de la retrouver plus tard dans votre page utilisateur. + + ## Notifications + + Quand quelqu'un vous répond, vous cite ou mentionne votre `@pseudo`, un numéro apparaîtra immédiatement en haut à droite de la page. Cliquez dessus pour accéder à vos **notifications**. + + + + N'ayez pas la crainte de manquer une réponse ; vous serez notifié par courriel de toutes les notifications survenant pendant que vous n'êtes pas connecté. + + ## Vos préférences + + - Tout article vieux de **moins de 2 jours** est considéré comme nouveau. + + - Toute conversation à laquelle vous avez **participé** (création, réponses ou lecture pendant une période prolongée) sera automatiquement suivie. + + En face de ces articles, vous verrez en bleu la mention "nouveau" ou bien le nombre de notifications non lues : + + + + Vous pouvez changer les paramètres de notification de chaque article en utilisant la commande de notification, en bas de l'article. + + + + Vous pouvez aussi configurer les notifications par catégorie, si vous voulez voir chaque nouvel article d'une catégorie spécifique. + + Pour changer n'importe lequel de ces paramètres, allez dans [vos préférences](%{base_url}/my/preferences). + + ## Une communauté basé sur la confiance + + Au fur et à mesure de votre participation, vous allez progressivement gagner la confiance de la communauté, vous deviendrez un membre à part entière et les limitations fonctionnelles propres à chaque nouvel utilisateur seront levées. + + À un niveau de confiance [suffisamment élevé](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924), vous aurez accès à de nouvelles fonctionnalités vous permettant de nous aider à gérer la communauté. welcome_user: subject_template: "Bienvenue sur %{site_name} !" text_body_template: "Merci d'avoir rejoint %{site_name} et bienvenue ! \n\n%{new_user_tips}\n\nNous croyons au [comportement communautaire civilisé](%{base_url}/guidelines) en tous temps. \n\nAmusez-vous bien ! \n\n(Si, en tant que nouvel utilisateur, vous avez besoin de communiquer avec un [responsable](%{base_url}/about), répondez simplement à ce message.)\n" @@ -1565,6 +1674,8 @@ fr: description: "Ces courriels ne vous intéressent pas ? Aucun problème ! Cliquez ci-dessous pour vous désabonner immédiatement :" reply_by_email: "[Visiter sujet](%{base_url}%{url}) ou répondre à ce courriel pour répondre." visit_link_to_respond: "[Visiter sujet](%{base_url}%{url}) pour répondre." + reply_by_email_pm: "[Visiter message](%{base_url}%{url}) ou répondre à ce courriel pour répondre." + visit_link_to_respond_pm: "[Visiter message](%{base_url}%{url}) pour répondre." posted_by: "Ecrit par %{username} le %{post_date}" user_invited_to_private_message_pm: subject_template: "[%{site_name}] %{username} vous a invité dans la conversation '%{topic_title}'" @@ -1665,6 +1776,7 @@ fr: --- %{respond_instructions} user_posted_pm_staged: + subject_template: "%{optional_re}%{topic_title}" text_body_template: |2 %{message} diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index dcfe7301db..ab3086d599 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -10,6 +10,8 @@ ja: short_date_no_year: "MMM D" short_date: "MMM D, YYYY" long_date: "MMMM D, YYYY h:mma" + date: + month_names: [null, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] title: "Discourse" topics: "トピック" posts: "投稿" @@ -19,6 +21,9 @@ ja: purge_reason: "アクティブでないアカウントは放棄されたとして削除されました" disable_remote_images_download_reason: "ディスク容量が不足しているため、リモートでの画像ダウンロードは無効になっています。" anonymous: "匿名" + emails: + incoming: + default_subject: "%{email}からメール" errors: &errors format: '%{attribute} %{message}' messages: @@ -35,8 +40,10 @@ ja: exclusion: は予約されています greater_than: は%{count}より大きい値にしてください greater_than_or_equal_to: は%{count}以上の値にしてください + has_already_been_used: "は既に使用されています" inclusion: は一覧にありません invalid: は不正な値です + is_invalid: "は不正です。もう少し説明を追加してください" less_than: は%{count}より小さい値にしてください less_than_or_equal_to: は%{count}以下の値にしてください not_a_number: は数値で入力してください @@ -168,7 +175,7 @@ ja: post: raw: "本文" user_profile: - bio_raw: "About Me" + bio_raw: "自己紹介" errors: models: topic: diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 160aca462f..fb549bbba8 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -90,6 +90,8 @@ pt_BR: not_found: "A URL ou recurso solicitado não pôde ser encontrado." invalid_access: "Você não tem permissão para ver o recurso solicitado." read_only_mode_enabled: "O site está em modo somente leitura. As interações estão desativadas." + reading_time: "Tempo de leitura" + likes: "Curtidas" too_many_replies: one: "Lamentamos, mas usuários novos estão temporariamente limitados a 1 resposta no mesmo tópico." other: "Lamentamos, mas usuários novos estão temporariamente limitados a %{count} respostas no mesmo tópico." @@ -295,6 +297,7 @@ pt_BR: one: "Não é possível excluir esta categoria porque ela tem um tópico. O tópico mais velho é %{topic_link}." other: "Não é possível excluir essa categoria porque ela tem %{count} tópicos. O tópico mais velho é %{topic_link}." topic_exists_no_oldest: "Não é possível excluir esta categoria porque contagem tópico é %{count}." + uncategorized_description: "Tópicos que não precisam de uma categoria, ou que não se encaixam em nenhuma outra categoria existente." trust_levels: newuser: title: "usuário novo" @@ -316,6 +319,7 @@ pt_BR: create_topic: "Você está criando tópicos muito rápido. Por favor aguarde %{time_left} antes de tentar novamente." create_post: "Você está respondendo muito rápido. Por favor aguarde %{time_left} antes de tentar novamente." topics_per_day: "Você atingiu o número máximo de novos tópicos hoje. Por favor aguarde %{time_left} antes de tentar novamente." + pms_per_day: "Você alcançou o número máximo de mensagens por hoje. Por favor aguarde %{time_left} antes de tentar de novo." create_like: "Você atingiu o número máximo de curtidas hoje. Por favor aguarde% {time_left} antes de tentar novamente." create_bookmark: "Você atingiu o número máximo de marcadores hoje. Por favor aguarde% {time_left} antes de tentar novamente." edit_post: "Você atingiu o número máximo de edições hoje. Por favor aguarde% {tempo} esquerda antes de tentar novamente." @@ -445,6 +449,8 @@ pt_BR: notify_moderators: title: "Algo mais" description: 'Esta postagem requer atenção da moderação por outra razão não listada acima.' + long_form: 'sinalizou isto para a atenção da equipe' + email_title: 'Um post em "%{title}" requer a atenção da equipe' email_body: "%{link}\n\n%{message}" bookmark: title: 'Adicionar Marcador' @@ -469,6 +475,7 @@ pt_BR: long_form: 'sinalizar como impróprio' notify_moderators: title: "Algo mais" + description: 'Este tópico requer a atenção geral da equipe baseado nas diretrizes da comunidade, no Termos de Serviço, ou em outra razão não listada acima.' long_form: 'sinalizar isso para atenção da moderação' email_title: 'O tópico "%{title}" requer atenção do moderador' email_body: "%{link}\n\n%{message}" diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index 415b548b5d..a1b6008449 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -10,6 +10,12 @@ ru: short_date_no_year: "D MMM" short_date: "D MMM YYYY" long_date: "D MMMM YYYY, HH:mm" + datetime_formats: &datetime_formats + formats: + short: "%m-%d-%Y" + date: + month_names: [null, Январь, Февраль, Март, Апрель, Май, Июнь, Июль, Август, Сентябрь, Октябрь, Ноябрь, Декабрь] + <<: *datetime_formats title: "Discourse" topics: "Темы" posts: "сообщения" @@ -446,29 +452,31 @@ ru: missing_session: "В данный момент, мы не можем подтвердить, была ли учетная запись создана. Убедитесь что в вашем браузере включены cookies и повторите попытку. В случае неудачи, обратитесь в техническую поддержку сайта." post_action_types: off_topic: - title: 'Не по теме' - description: 'Это сообщение не имеет отношения к данному обсуждению. Возможно, его стоит перенести в соответствующий раздел.' - long_form: 'отмечена как не по теме' + title: 'Офтопик (не по теме)' + description: 'Это сообщение не имеет отношения к теме, учитывая ее название и текст. Возможно, его стоит перенести.' + long_form: 'отметили это как офтопик (не по теме)' spam: - title: 'СПАМ' - description: 'Это реклама! Данное сообщение не представляет особого интереса или не относится к начальной теме. ' - long_form: 'отмечено как СПАМ' - email_title: '"%{title}" отмечено как спам' + title: 'Спам' + description: 'Это сообщение носит рекламный характер и не представляет особого интереса для темы.' + long_form: 'отметили это как спам' + email_title: 'Отмечено как спам: "%{title}"' email_body: "%{link}\n\n%{message}" inappropriate: - title: 'Неуместно' + title: 'Неприемлемо' description: 'Это сообщение может быть оскорбительным или нарушает правила поведения.' long_form: 'отметить как неуместное' notify_user: - title: 'Отправить @{{username}} сообщение' - description: 'Я хочу пообщаться приватно с этим человеком о его посте.' + title: 'Отправить @{{username}} личное сообщение' + description: 'Я хочу обсудить это сообщение с автором приватно и не привлекая внимания модераторов.' long_form: 'оповещаемый пользователь' email_title: 'Ваше сообщение в теме "%{title}"' email_body: "%{link}\n\n%{message}\n" notify_moderators: - title: "Другое" - description: 'Этот пост требует внимания администрации по другой причине.' - email_body: "%{link}\n\n%{message}\n" + title: "Другая причина" + description: 'Это сообщение требует модерации по иной причине, которая отсутствует в списке выше.' + long_form: 'отметили это для модерации' + email_title: 'Сообщение в теме "%{title}" требует модерации' + email_body: "%{link}\n\n%{message}" bookmark: title: 'Добавить в закладки' description: 'Добавить сообщение в закладки' @@ -1040,6 +1048,17 @@ ru: redirected_to_top_reasons: new_user: "Добро пожаловать в наше сообщество! Вот самые популярные недавние темы." not_seen_in_a_month: "С возвращением! Т.к. вас не было какое-то время, мы собрали список популярных тем за время вашего отсутсвия. Вот они." + move_posts: + new_topic_moderator_post: + one: "Сообщение перенесено в новую тему: %{topic_link}" + few: "%{count} сообщения перенесены в новую тему: %{topic_link}" + many: "%{count} сообщений перенесены в новую тему: %{topic_link}" + other: "%{count} сообщений перенесены в новую тему: %{topic_link}" + existing_topic_moderator_post: + one: "Сообщение перенесено в тему %{topic_link}" + few: "%{count} сообщения перенесены в тему %{topic_link}" + many: "%{count} сообщений перенесены в тему %{topic_link}" + other: "%{count} сообщений перенесены в тему %{topic_link}" change_owner: post_revision_text: "Владелец сменен с %{old_user} на %{new_user}" deleted_user: "Удаленный пользователь" @@ -1291,6 +1310,8 @@ ru: Тем не менее, если сообщение окажется скрытым второй раз, об этом будут оповещены модераторы, которые могут отреагировать на жалобы, вплоть до приостановления доступа к вашей учетной записи. Дополнительная информация доступна в [часто задаваемых вопросах](%{base_url}/guidelines). + usage_tips: + text_body_template: "Несколько советов, чтобы помочь вам освоиться на форуме:\n\n## Чтение\n\nЗдесь нет кнопок \"Следующая страница\" или номеров страниц -- чтобы читать дальше, просто прокручивайте страницу вниз, и следующие темы и сообщения будут автоматически подгружаться.\n\n## Навигация\n\n- Обратите внимание на **иконки в правом верхнем углу страницы**. Там есть поиск по форуму, меню и ссылка на вашу пользовательскую страницу с индивидуальными настройками. \n\n- Если нажать на заголовок любой темы в списке тем, вы попадёте к **первому непрочитанному сообщению** в этой теме. Если же вы хотите перейти в начало или конец темы, то вместо заголовка следует кликнуть на количество ответов или на дату последнего ответа в этой теме, соответственно:\n\n \n\n- Для быстрого перемещения к нужному сообщению внутри темы используйте индикатор с номерами постов в правом нижнем углу страницы:\n\n \n\n- Чтобы перейти в самое начало темы, можно кликнуть на заголовок наверху.\n\n- Перемещаться по форуму можно также с помощью \"горячих клавиш\". Нажмите ?, чтобы ознакомиться с их списком.\n\n## Как отвечать\n\n- Чтобы **ответить в теме**, без привязки к какому-либо предыдущему комментарию в ней, следует нажать на кнопку в самом низу на странице темы.\n\n- Чтобы ответить **конкретному человеку**, надо нажать на иконку внизу поста, на который вы хотите ответить.\n\n- Ещё есть возможность ответить на сообщение новой темой. Так имеет смысл делать, когда обсуждение значительно отклоняется от темы, заявленной в заголовке. Чтобы ответить **новой темой**, нажмите на знак справа от поста, на который вы отвечаете. Система автоматически свяжет старую и новую темы между собой ссылками.\n\nДля форматирования текста в сообщениях можно использовать HTML, BBCode и [Markdown](https://ru.wikipedia.org/wiki/Markdown). Например, чтобы выделить фрагмент текста жирным шрифтом, можно использовать любой из следующих вариантов разметки:\n\n Этот текст **будет жирным**.\n Этот текст будет жирным.\n Этот текст [b]будет жирным[/b].\n\nЧтобы при ответе использовать цитату из другого сообщения, выделите текст, который вы хотите процитировать, и нажмите кнопку \"Ответить\". И так можно проделать для нескольких цитат.\n\n\n\nЧтобы оповестить определённого участника форума о вашем сообщении, упомяните в тексте его имя пользователя (\"ник\"), начиная со знака @. Наберите `@`, и откроется список пользователей, из которого вы сможете выбрать адресата.\n\n\n\nЧтобы использовать [иконки Emoji](http://www.emoji.codes/), достаточно ввести с клавиатуры символ `:`, и откроется меню с выбором иконок. Разумеется, можно и вручную вводить традиционные смайлики `;)`\n\n\n\nЕсли вы хотите поделиться ссылкой, то можно сделать так, чтобы в сообщении автоматически показывался небольшой фрагмент с той страницы, куда ведёт ссылка. Для этого надо эту ссылку размещать в отдельной строке сообщения, не внутри текста:\n\n\n\n## Дополнительные возможности\n\nВнизу каждого сообщения есть кнопки действий:\n\n\n\nЕсли вам понравился чей-либо пост, обязательно поблагодарите автора, нажав на кнопочку с сердечком (**\"мне нравится\"**).\n\nЕсли же какое-либо сообщение вам кажется неприемлемым или нарушающим [правила форума](%{base_url}/guidelines), пожалуйста, сообщите об этом приватно автору этого сообщения или [команде форума](%{base_url}/about) (по вашему усмотрению) с помощью кнопки c флажком (\"**пожаловаться**\").\n\nКнопка с цепочкой позволяет получить ссылку на пост, чтобы сохранить её или **поделиться** через социальную сеть.\n\nИ, наконец, есть кнопка с изображением книжной закладки, которая позволяет сохранить пост в **избранное** и потом быстро вернуться к нему со своей страницы пользователя.\n\n## Оповещения\n\nКогда кто-нибудь отвечает вам, цитирует ваше сообщение или упоминает по `@имени_пользователя`, в правом верхнем углу страницы появится иконка с цифрой. Эта цифра означает количество ожидаемых вас **оповещений**. Кликните на неё, чтобы увидеть список.\n\n\n\nЕсли новое оповещение появится когда вас нет на сайте, то оно продублируется на электронную почту. На всякий случай, проверьте свои [настройки оповещений](%{base_url}/my/preferences), чтобы убедиться, что там всё как вам удобно.\n\n## Настройки\n\n - По умолчанию, темы будут считаться новыми, если они созданы **за последние два дня**.\n\n - Любая тема, в которой вы **активно участвовали** (отвечали, создали эту тему или просто читали её достаточно долгое время) будет автоматически отслеживаться на предмет новых сообщений.\n\nВы увидите синий индикатор \"новый\" и число новых сообщений рядом с отслеживаемыми темами.\n\n\n\nВы можете изменить настройки уведомлений и отслеживания отдельно для любой темы с помощью меню внизу этой темы:\n\n\n\nАналогичным образом можно настроить желаемый режим оповещений для разделов форума, чтобы получать оповещения о новых темах в этих разделах.\n\nОбщие для всех тем настройки отслеживания и классификация тем как \"новые\" задаются в [пользовательских настройках](%{base_url}/my/preferences).\n" welcome_user: subject_template: "Добро пожаловать на %{site_name}!" text_body_template: | @@ -1690,6 +1711,8 @@ ru: performance_report: initial_post_raw: Эта тема содержит ежедневные отчеты активности форума. initial_topic_title: Отчеты активности форума + time: + <<: *datetime_formats activemodel: errors: <<: *errors diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 2b2431e3fd..5ad1ce158b 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -27,6 +27,9 @@ sk: purge_reason: "Automaticky vymazaný ako opustený, neaktivovaný účet" disable_remote_images_download_reason: "Sťahovanie vzdialených obrázkov je vypnuté kvôli nedostatku diskového priestoru." anonymous: "Anonymný" + emails: + incoming: + default_subject: "Prijatý email od %{email}" errors: &errors format: '%{attribute} %{message}' messages: @@ -322,6 +325,7 @@ sk: few: "Nemôžte vymazať kategóriu pretože obsahuje %{count} témy. Najstaršia téma je %{topic_link}." other: "Nemôžte vymazať kategóriu pretože obsahuje %{count} tém. Najstaršia téma je %{topic_link}." topic_exists_no_oldest: "Nemôžte vymazať túto kategóriu pretože obsahuje %{count} tém." + uncategorized_description: "Témy, ktoré nepotrebujú kategóriu, alebo sa do žiadnej existujúcej nehodia." trust_levels: newuser: title: "nový používateľ" @@ -951,6 +955,7 @@ sk: max_users_notified_per_group_mention: "Maximálny počet používateľov, ktorý môžu dostať notifikácie, v prípade notifikovania celej skupiny (ak sa dosiahne maximum, žiadne ďalšie notifikácie nebudú zaslané)" create_thumbnails: "Vytvor náhľad a okraje pre obrázoky, ktoré sú príliš veľké aby sa zmestili do príspevku." email_time_window_mins: "Počkať (n) minút pred poslaním akýchkoľvek notifikačných emailov, aby mali používatelia čas úpraviť a dokončiť svoje príspevky." + private_email_time_window_seconds: "Počkať (n) sekúnd pred poslaním akýchkoľvek súkromných notifikačných emailov, aby mali používatelia čas úpraviť a dokončiť svoje príspevky." email_posts_context: "Koľko posledných odpovedí zahrnúť do obsahu v notifikačných emailoch." flush_timings_secs: "Ako často posielať časové dáta na server, v sekundách." title_max_word_length: "Maximálna povolená dĺžka slov v názve témy, v znakoch." @@ -1007,6 +1012,7 @@ sk: display_name_on_email_from: "V emailovom poli Od zobraziť plné mená" unsubscribe_via_email: "Umožniť používateľom odhlásiť sa z emailov pomocou zaslania emailu s textom 'unsubscribe' v predmete alebo v tele emailu" unsubscribe_via_email_footer: "Do päty stránky posielaných emailov pridať odkaz na odhlásenie sa" + delete_email_logs_after_days: "Zmazať logy o emailoch po (N) dňoch. 0 pre neobmedzene. " pop3_polling_enabled: "Získavať emailové odpovede pomocou POP3" pop3_polling_ssl: "Na spojenie s POP3 serverom použiť SSL. (Doporučované)" pop3_polling_period_mins: "Doba v minutách medzi POP3 kontrolami emailov. POZNÁMKA: vyžaduje reštart." @@ -1033,7 +1039,7 @@ sk: automatically_download_gravatars: "Po vytvorení účtu alebo zmene heslo stiahnuť používateľov Gravatar." digest_topics: "Maximálny počet tém, ktoré sa zobrazia v súhrnnom emaile." digest_min_excerpt_length: "Minimálny výťah príspevku v sumarizačnom emaile, v znakoch." - delete_digest_email_after_days: "Zakázat súhrnný email používateľom, ktorý neboli na stránkach viac ako (n) dní." + delete_digest_email_after_days: "Zakázat súhrnný email používateľom, ktorí neboli na stránkach viac ako (n) dní." disable_digest_emails: "Vypnúť sumárny email pre všetkých používateľov." detect_custom_avatars: "Má alebo nemá sa kontrolovať nahratie vlastného profilového obrázka používateľa?" max_daily_gravatar_crawls: "Koľkokrát denne má Discourse kontrolovať Gravatar na zistenie vlastného avatara." @@ -1546,12 +1552,12 @@ sk: ``` csv_export_succeeded: subject_template: "Export dát kompletný" + csv_export_failed: + subject_template: "Export dát zlyhal" + email_reject_inactive_user: + subject_template: "[%{site_name}] Problém s emailom -- Neaktívny užívateľ" email_reject_no_account: subject_template: "[%{site_name}] Problém s emailom -- Neznámy účet" - text_body_template: | - Prepáčte, ale Vaša emailová správa na %{destination} (titled %{former_title}) nefungovala. - - S touto emailovou adresou neexistuje žiadny používateľský účet. Pokúste za zaslať správu pomocou inej email adresy alebo kontaktujte zamestnanca stránok. email_reject_empty: subject_template: "[%{site_name}] Problém s emailom -- Žiadny obsah" text_body_template: | @@ -1572,18 +1578,18 @@ sk: Prepáčte, ale Vaša emailová správa na %{destination} (titled %{former_title}) nefungovala. Na založenie novej témy v tejto kategórií Váš účet nemá dostatočné oprávnenia. Ak si myslíte, že ide o chybu, kontaktujte zamestnanca stránok. + email_reject_strangers_not_allowed: + subject_template: "[%{site_name}] Problém s emailom -- Nesprávny prístup" + email_reject_invalid_post: + subject_template: "[%{site_name}] Problém s emailom -- Chyba pri uverejnení" + email_reject_invalid_post_specified: + subject_template: "[%{site_name}] Problém s emailom -- Chyba pri uverejnení" email_reject_reply_key: subject_template: "[%{site_name}] Problém s emailom -- Neznámy kľúč odpovede" - text_body_template: | - Prepáčte, ale Vaša emailová správa na %{destination} (titled %{former_title}) nefungovala. - - Poskytnutý kľúč odpovede je neplatný, alebo neznámy, takže nedokážeme určiť, k čomu táto emailová odpoveď patrí. Kontaktujte zamestnanca stránok. + email_reject_bad_destination_address: + subject_template: "[%{site_name}] Problém s emailom -- Neznáma adresa príjemcu - Komu:" email_reject_topic_not_found: subject_template: "[%{site_name}] Problém s emailom -- Téma nebola nájdená" - text_body_template: | - Prepáčte, ale Vaša emailová správa na %{destination} (titled %{former_title}) nefungovala. - - Téma, na ktorú odpovedáte už neexistuje. možno bola vymazaná Ak si mysíte, že ide o chybu, kontaktujte zamestnanca stránok. email_reject_topic_closed: subject_template: "[%{site_name}] Problém s emailom -- Téma je uzavretá" text_body_template: | @@ -1733,6 +1739,10 @@ sk: subject_template: "[%{site_name}] Váš nový účet" authorize_email: subject_template: "[%{site_name}] Potvrďte Vašu novú email adresu" + text_body_template: | + Potvrďte Vašu novú emailovú adresu pre %{site_name} kliknutím na nasledujúci odkaz: + + %{base_url}/users/authorize-email/%{email_token} signup_after_approval: subject_template: "Váš účet bol schválený na %{site_name}!" signup: diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index e02c1a55d9..094d7b1162 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -10,6 +10,14 @@ vi: short_date_no_year: "D MMM" short_date: "D MMM, YYYY" long_date: "MMMM D, YYYY h:mma" + datetime_formats: &datetime_formats + formats: + short: "%m-%d-%Y" + short_no_year: "%B %-d" + date_only: "%b %-d, %Y" + date: + month_names: [null, Tháng Một, Tháng Hai, Tháng Ba, Tháng Tư, Tháng Năm, Tháng Sáu, Tháng Bảy, Tháng Tám, Tháng Chín, Tháng Mười, Tháng Mười Một, Tháng Mười Hai] + <<: *datetime_formats title: "Discourse" topics: "Chủ đề" posts: "bài viết" @@ -19,11 +27,14 @@ vi: purge_reason: "Tự động xóa tài khoản không sử dụng, không kích hoạt." disable_remote_images_download_reason: "Không thể tải ảnh về máy chủ vì thiếu dung lượng." anonymous: "Ẩn danh" - errors: + emails: + incoming: + default_subject: "Email đến từ %{email}" + errors: &errors format: '%{attribute} %{message}' messages: too_long_validation: "cho phép tối đa %{max} ký tự; bạn đã nhập %{length}." - invalid_boolean: "Giá trị boolean không hợp lệ" + invalid_boolean: "Giá trị logic không hợp lệ" taken: "đã được lấy trước" accepted: phải được chấp nhận blank: không thể để rỗng @@ -78,6 +89,8 @@ vi: not_found: "Không thể tìm thấy đường dẫn hoặc tài nguyên yêu cầu." invalid_access: "Bạn không được phép xem tài nguyên đã yêu cầu." read_only_mode_enabled: "Trang web đang ở chế độ chỉ đọc. Tất cả các tương tác đã bị tắt." + reading_time: "Thời gian đọc" + likes: "Lượt Thích" too_many_replies: other: "Xin lỗi bạn, người dùng mới tạm thời bị giới hạn với %{count} câu trả lời trong một chủ đề." embed: @@ -92,6 +105,20 @@ vi: replies: other: "%{count} câu trả lời" no_mentions_allowed: "Xin lỗi, bạn không thể nhắc tới thành viên khác." + too_many_mentions: + other: "Xin lỗi, bạn chỉ có thể nhắc tới %{count} thành viên trong một bài viết." + no_mentions_allowed_newuser: "Xin lỗi, thành viên mới không thể nhắc tới thành viên khác." + too_many_mentions_newuser: + other: "Xin lỗi, thành viên mới chỉ có thể nhắc tới %{count} thành viên khác trong một bài viết." + no_images_allowed: "Xin lỗi, thành viên mới chưa được chèn hình ảnh vào bài viết." + too_many_images: + other: "Xin lỗi, thành viên mới chỉ có thể chèn %{count} hình ảnh trong một bài viết." + no_attachments_allowed: "Xin lỗi, thành viên mới chưa được chèn tập tin trong bài viết." + too_many_attachments: + other: "Xin lỗi, thành viên mới chỉ được chèn %{count} file đính kèm trong một bài viết." + no_links_allowed: "Xin lỗi, thành viên mới chưa được chèn liên kết trong bài viết." + too_many_links: + other: "Xin lỗi, thành viên mới chỉ được chèn %{count} liên kết trong một bài viết." spamming_host: "Xin lỗi bạn không thể chèn liên kết tới trang đó." user_is_suspended: "Người dùng đang bị treo không được phép đăng bài." topic_not_found: "Có gì đó đã sai. Có lẽ chủ đề này đã bị đóng hoặc bị xóa trong khi bạn đang xem?" @@ -123,6 +150,8 @@ vi: errors: can_not_modify_automatic: "Bạn không thể sửa đổi một nhóm tự động" member_already_exist: "'%{username}' đã là thành viên của nhóm" + invalid_domain: "'%{domain}' không phải là tên miền hợp lệ." + invalid_incoming_email: "'%{incoming_email}' không phải là một địa chỉ mail hợp lệ." default_names: everyone: "Mọi người" admins: "quản trị" @@ -209,6 +238,7 @@ vi: attributes: hex: invalid: "không phải là một màu không hợp lệ" + <<: *errors user_profile: no_info_me: "
      Mục nói về bản thân bạn trong hồ sơ của bạn hiện đang trống, bạn muốn điền vào nó? " no_info_other: "
      %{name} đã không nhập bất cứ điều gì nói về bản thân của họ " @@ -244,6 +274,8 @@ vi: [trust]: https://meta.discourse.org/t/what-do-user-trust-levels-do/4924 category: topic_prefix: "Giới thiệu chuyên mục %{category}" + replace_paragraph: "(Thay thế đoạn này với miêu tả về category mới. Bản hướng dẫn này sẽ được xuất hiện trong vùng chọn của category, vì thế nó chỉ giới hạn trong 200 kí tự. **Cho đến khi bạn chỉnh sửa phần miêu tả này hoặc tạo topic cho nó, category này sẽ không xuất hiện trên trang web.**)" + post_template: "%{replace_paragraph}\n\nSử dụng những đoạn sau để viết mô tả dài, hướng dẫn sử dụng hoặc điều luật sử dụng:\n\n- Mục đích sử dụng của category này là gì?\n\n- Category này khác với những cái khác như thế nào?\n\n- Những topic trong category này thường có nội dung gì?\n\n- Chúng ta có thể ghép với category khác không?\n" errors: uncategorized_parent: "Mục \"Chưa được phân loại\" không thể có một chuyên mục chính" self_parent: "Cha của chủ đề phụ không thể nào là chính nó" @@ -255,14 +287,32 @@ vi: topic_exists: other: "Không thể xoá phân loại này được bởi vì nó có %{count} chủ đề. Các chủ đề cũ là %{topic_link}." topic_exists_no_oldest: "Không thể xoá chuyên mục này vì nó có %{count} chủ để." + uncategorized_description: "Chủ đề không cần chuyên mục, hoặc không phù hợp với bất kỳ chuyên mục nào hiện có." trust_levels: newuser: title: "thành viên mới" basic: title: "thành viên cơ bản" + member: + title: "thành viên" + regular: + title: "thường xuyên" + leader: + title: "người khởi xướng" change_failed_explanation: "Bạn đã cố gắng để giảm hạng %{user_name} xuống '%{new_trust_level}'. Tuy nhiên cấp độ tin cậy hiện tại của họ đã là '%{current_trust_level}'. %{user_name} sẽ được giữ lại ở cấp độ '%{current_trust_level}' - nếu bạn muốn giảm hạng thành viên, trước tiên hãy khóa cấp độ tin cậy" rate_limiter: + slow_down: "Hành động này đã được thực hiện quá nhiều lần. Bạn vui lòng thử lại sau." too_many_requests: "Hành động bạn vừa thực hiện bị giới hạn theo ngày. Hãy chờ %{time_left} và thử lại." + by_type: + first_day_replies_per_day: "Bạn vừa vượt quá số lần trả lời tối đa trong ngày đầu của thành viên mới. Xin hãy chờ %{time_left} và thử lại sau." + first_day_topics_per_day: "Bạn vừa đạt tới số lần mở topic tối đa cho thành viên mới. Xin vui lòng chờ %{time_left} trước khi thử lại." + create_topic: "Bạn đang tạo topic quá nhanh. Vui lòng chờ %{time_left} trước khi thử lại." + create_post: "Bạn đang trả lời quá nhanh. Xin vui lòng chờ %{time_left} trước khi thử lại." + topics_per_day: "Bạn vừa đạt tới số lần mở topic mới tối đa trong ngày hôm nay. Vui lòng chờ %{time_left} trước khi thử lại." + pms_per_day: "Bạn vừa đạt tới số lần gửi tin nhắn tối đa trong ngày. Xin vui lòng chờ %{time_left} trước khi thử lại." + create_like: "Bạn vừa đạt tới số lần tối đa được Like trong ngày. Xin vui lòng chờ %{time_left} trước khi thử lại." + create_bookmark: "Bạn vừa đạt tới số lần đánh dấu tối đa trong ngày. Xin vui lòng chờ %{time_left} trước khi thử lại." + edit_post: "Bạn vừa đạt tới số lần chỉnh sửa tối đa trong ngày. Xin vui lòng chờ %{time_left} trước khi thử lại." hours: other: "%{count} giờ" minutes: @@ -356,11 +406,16 @@ vi: description: 'Chủ để này chứa nội dung mà bình thường được xem là xúc phạm, lạm dụng, hoặc vi phạm nguyên tắc cộng đồng.' long_form: 'đánh dấu cái này không thích hợp' notify_user: + title: 'Gửi tin nhắn cho @{{username}}.' + description: 'Tôi muốn trao đổi riêng, trực tiếp với người này về bài viết của họ.' long_form: 'đã nhắn tin cho thành viên' email_title: 'Bài đăng của bạn trong "%{title}"' email_body: "%{link}\n\n%{message}" notify_moderators: title: "Một thứ khác" + description: 'Bài viết này cần được Ban quản trị chú ý. Lý do chưa được liệt kê trong danh sách.' + long_form: 'đã đánh dấu thông báo cho Ban quản trị' + email_title: 'Một bài viết trong "%{title}" cần Ban quản trị lưu ý' email_body: "%{link}\n\n %{message}" bookmark: title: "Đánh dấu chỉ mục \x1C" @@ -385,6 +440,7 @@ vi: long_form: 'đã đánh dấu bài này không phù hợp' notify_moderators: title: "Một cái khác" + description: 'Bài viết này cần Ban quản trị lưu ý theo dựa trên hướng dẫnđiều khoản sử dụng.' long_form: 'đã đánh dấu cho điều hành viên xem xét' email_title: 'Chủ đề "%{title}" cần được ban điều hành quan tâm' email_body: "%{link}\n\n%{message}" @@ -557,6 +613,7 @@ vi: host_names_warning: "Cài đặt của bạn config/database.yml đang sử dụng hostname mặc định. Cập nhật lại để sử dụng hostname của bạn" gc_warning: 'Máy chủ của bạn hiện tại sử dụng cơ chế dọn rác mặc định của ruby, điều này khiến cho hiệu năng của máy chủ không tốt lắm. Đọc chủ đề sau cho việc tối ưu hiệu năng Tối ưu Ruby and Rails cho Discourse.' sidekiq_warning: ' Sidekiq đang không hoạt động. Rất nhiều tác vụ, như gửi email, là được thực thi không đồng bộ bởi sidekiq. Hãy chắc chắn rằng ít nhất một tiến trình sidekiq phải đang hoạt động. Đọc thêm về Sidekiq tại đây.' + queue_size_warning: 'Có %{queue_size} công việc đang chờ xử lý trong hàng đợi. Điều này chứng tỏ có vấn đề đã xảy ra với tiến trình Sidekiq, hoặc bạn cần tăng số lượng Sidekiq workers ().' memory_warning: 'Máy chủ của bạn có bộ nhớ ít hơn 1 GB. Khuyến cáo sử dụng bộ nhớ tối thiểu 1 GB .' google_oauth2_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Google OAuth2 (enable_google_oauth2_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' facebook_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Facebook (enable_facebook_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' @@ -567,9 +624,14 @@ vi: image_magick_warning: 'Máy chủ đã cấu hình để tạo hình đại diện nhỏ từ những hình lới, nhưng ImageMagick chưa được cài đặt. Cài ImageMagick sử dụng trình quản lý package yêu thích của bạn hoặc tải về phiên bản mới nhất.' failing_emails_warning: 'Có %{num_failed_jobs} email jobs thấ bại. Kiểm tra app.yml và chắc chắn rằng cấu hình máy chủ email đúng. Xem jobs thất bại ở Sidekiq.' default_logo_warning: "Cập nhập logo của trang. Cập nhập logo_url, logo_small_url, và favicon_url trong Thiết lập trang." + contact_email_missing: "Điền địa chỉ email liên hệ để có thể nhận được các vấn đề cấp bách liên quan đến website của bạn, cập nhật trong Thiết lập website." contact_email_invalid: "Email liên lạc của trang không hợp lệ. Cập nhật trong Thiết lập trang/a>." title_nag: "Nhập tên trang của bạn. Cập nhập tiêu đề trong Thiết lập trang." + site_description_missing: "Điền một câu mô tả về website để hiển thị trên trang kết quả tìm kiếm, cập nhật site_description trong Thiết lập website." consumer_email_warning: "Trang web của bạn được cài đặt sử dụng Gmail (hoặc một dịch vụ email khác) để gửi email. Gmail có giới hạn số lượng email bạn có thể gửi. Hãy xem xét sử dụng một dịch vụ email khác như mandrill.com để đảm bảo khả năng vận chuyển tất cả các email." + site_contact_username_warning: "Điền tên tài khoản điều hành viên để gửi tin nhắn tự động, cập nhật site_contact_username trong Thiết lập website." + notification_email_warning: "Email thông báo không thể gửi từ một địa chỉ email với tên miền của bạn, gửi email sẽ thất thường và không đáng tin cậy. Hãy thiết lập notification_email tới một địa chỉ tin cậy trong Thiết lập website." + subfolder_ends_in_slash: "Thư mục con của bạn được thiết lập không đúng, DISCOURSE_RELATIVE_URL_ROOT phải được kết thúc bằng dấu gạch chéo." site_settings: censored_words: "Từ sẽ tự động thay thế bằng ■■■■" delete_old_hidden_posts: "Tự động ẩn bất kỳ bài viết ở ẩn hơn 30 ngày." @@ -583,9 +645,11 @@ vi: max_topic_title_length: "Số kí tự tối đa trong tiêu đề chủ đề." min_private_message_title_length: "Chiều dài tối thiểu cho phép theo số kí tự của một thông điệp" min_search_term_length: "Số kí tự tối thiểu trong từ khóa tìm kiếm." - uncategorized_description: "Mô tả của chuyên mục \"Không phân loại\". Để trống khi không muốn mô tả." + search_tokenize_chinese_japanese_korean: "Bắt buộc tìm kiếm tách từ Chinese/Japanese/Korean ngay cả trên các site không phải là CJK" + allow_uncategorized_topics: "Cho phép các chủ đề được tạo ra mà không gán chuyên mục. LƯU Ý: Nếu có bất kỳ chủ đề nào chưa gán chuyên mục, bạn phải phân loại chúng trước khi thay đổi." allow_duplicate_topic_titles: "Cho phép các chủ đề trùng tiêu đề." unique_posts_mins: "Trong bao nhiêu phút người sử dụng có thể viết bài khác với nội dung giống nhau" + educate_until_posts: "Khi người dùng bắt đầu đăng (n) bài viết đầu tiên, hiện pop-up để hướng dẫn họ khi soạn thảo." title: "Tên của trang này, sử dụng trong thẻ tiêu đề" site_description: "Mô tả trang này trong một câu, nó sẽ được sử dụng trong thẻ meta description" contact_email: "Địa chỉ email liên hệ của người chịu trách nhiệm trang này. Sử dụng cho những thông báo quan trọng giống như cờ không được quản lý, cũng giống form liện hệ /about cho những vấn đề cấp bách." @@ -595,6 +659,7 @@ vi: download_remote_images_to_local: "Tải ảnh về lưu trữ để tránh ảnh bị hư." download_remote_images_threshold: "Dung lượng tối thiểu cần để tải ảnh từ xa về lưu trữ (tính bằng phần trăm)" disabled_image_download_domains: "Tải ảnh từ xa sẽ không áp dụng với các tên miền sau. Phân cách bằng dấu |" + editing_grace_period: "Trong khoảng (n) giây sau khi gửi bài, chỉnh sửa sẽ không tạo ra một phiên bản mới trong bài lịch sử bài viết." post_edit_time_limit: "Tác giả có thể sửa hoặc xóa bài viết của họ trong (n) phút sau khi đăng. 0 là mãi mãi." edit_history_visible_to_public: "Cho phép mọi người nhìn thấy phiên bản trước khi chỉnh sửa bài viết. Khi không cho phép, chỉ nhân viên có thể xem." delete_removed_posts_after: "Bài viết đã được xóa bởi tác giả sẽ được tự động xóa sau (n) giờ. Nếu cài là 0, bài viết sẽ được xóa ngay lập tức." @@ -604,10 +669,18 @@ vi: show_subcategory_list: "Hiện danh sách chuyên mục con thay vì danh sách chủ đề khi truy cập vào chuyên mục." fixed_category_positions: "Nếu được bật, bạn sẽ có thể sắp xếp chuyên mục theo một thứ tự cố định. Nếu không bật, chuyên mục sẽ được sắp xếp theo thứ tử hoạt động." fixed_category_positions_on_create: "Nếu chọn, sắp xếp danh mục sẽ được thực hiện trong cửa sổ tạo chủ đề (yêu cầu fixed_category_positions)." + add_rel_nofollow_to_user_content: "Thêm rel='nofollow' cho tất cả các nội dung mà người dùng gửi, ngoại trừ các liên kết nội bộ (của tên miền chính). Nếu thay đổi, bạn phải thực hiện lại cho tất cả các bài viết với: \"rake posts:rebake\"" + exclude_rel_nofollow_domains: "Một danh sách tên miền mà rel='nofollow' không được thêm vào các liên kết, các tên miền con sẽ được tự động áp dụng như với tên miền chính. Tối thiểu, bạn nên thêm tên miền cấp cao nhất của website này để giúp search engine tìm kiếm tất cả các nội dung. Nếu các phần khác của website thuộc tên miền khác, cũng nên thêm vào." post_excerpt_maxlength: "Chiều dài tối đa của đoạn trích / tóm tắt chủ đề." + post_onebox_maxlength: "Số ký tự tối đa của một bài onebox Discourse." + onebox_domains_whitelist: "Danh sách các tên miền cho phép onebox, các tên miền này phải hỗ trợ OpenGraph hoặc oEmbed. Kiểm tra tại http://iframely.com/debug" + logo_url: "Logo ở vị trí trên cùng của website nên là hình chữ nhật, nếu để trống thì tên website sẽ hiển thị." + digest_logo_url: "Logo thay thế đặt ở trên cùng của tập san email nên là hình chữ nhật, nếu để trống thì 'logo_url' sẽ được sử dụng." + logo_small_url: "Logo nhỏ ở vị trí trên cùng của website hiển thị khi cuộn xuống nên là hình vuông, nếu để trống thì icon trang chủ sẽ hiển thị." favicon_url: "Favicon cho trang của bạn, xem tại http://en.wikipedia.org/wiki/Favicon, để chạy được với CDN ảnh phải là png" mobile_logo_url: "Cố định vị trí hình logo sử dụng tại phía trên bên trái trang mobile của bạn. Nên là hình vuông. Nếu để trống, sẽ sử dụng `logo_url`. Ví dụ: http://example.com/uploads/default/logo.png" apple_touch_icon_url: "Biểu tượng sử dụng trong các thiết bị cảm ứng của Apple. Kích thước gợi ý 144px x 144px" + notification_email: "Địa chỉ email 'Từ:' được dùng để gửi các email thiết yếu của hệ thống. Các tên miền quy định ở đây phải có SPF, DKIM và bản ghi PTR phải được thiết lập chính xác cho email đến." email_custom_headers: "Danh sách xác định email header tùy chỉnh" email_subject: "Tùy biến định dạng chủ đề cho chuẩn email. Xem tại https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" use_https: "URL đầy đủ đến trang (Discourse.base_url) là http hoặc https? KHÔNG BẬT NÓ CHO TỚI KHI HTTPS ĐÃ CÀI ĐẶT SẴN SẰNG VÀ ĐÃ CHẠY!" @@ -616,12 +689,29 @@ vi: summary_likes_required: "Số lượt thích trong một chủ đề trước khi 'Tóm tắt chủ đề này' được kích hoạt" summary_percent_filter: "Khi người dùng nhấn 'Tóm tắt chủ đề này', hiển thị phí trên % của bài viết" summary_max_results: "Số bài viết tối đa trả ra bởi 'Tóm tắt chủ đề này'" + enable_private_messages: "Cho phép thành viên có cấp độ tin cậy mức 1 (cấu hình thông qua cấp độ tin cậy tối thiểu khi gửi tin nhắn) tạo và trả lời tin nhắn" + enable_long_polling: "Message Bus sử dụng để thông báo có thể sử dụng vòng gọi dài" + long_polling_base_url: "URL chính sử dụng cho vòng gọi dài (khi một CDN phục vụ nội dung động, hãy chắc chắn để thiết lập này là vòng gọi gốc), vd: http://origin.site.com" + long_polling_interval: "Thời gian server phải đợi trước khi gửi trả lời khi không có dữ liệu để gửi (chỉ với tài khoản đăng nhập)" + polling_interval: "When not long polling, how often should logged on clients poll in milliseconds" + anon_polling_interval: "How often should anonymous clients poll in milliseconds" + background_polling_interval: "How often should the clients poll in milliseconds (when the window is in the background)" + flags_required_to_hide_post: "Số lần đánh dấu để bài viết tự động ẩn và gửi tin nhắn đến người dùng (điền 0 để tắt)" cooldown_minutes_after_hiding_posts: "Số phút một người dùng phải chờ trước khi họ có thể sửa một bài viết ẩn bởi gắn cờ cộng đồng" max_topics_in_first_day: "Số chủ đề tối đa một thành viên được tạo trong ngày đầu tiên." max_replies_in_first_day: "Số trả lời tối đa một thành viên được tạo trong ngày đầu tiên" tl2_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 2 (thành viên) bằng cách nhân với số này" tl3_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 3 (bình thường) bằng cách nhân với số này" tl4_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 4 (dẫn đầu) bằng cách nhân với số này" + num_flags_to_block_new_user: "Nếu bài viết của thành viên mới nhận được nhiều đánh dấu spam từ num_users_to_block_new_user thành viên khác, ẩn tất cả các bài viết của họ và ngăn chặn gửi bài trong tương lai, điền 0 để tắt." + num_users_to_block_new_user: "Nếu bài viết của thành viên mới nhận được num_flags_to_block_new_user đánh dấu spam từ các thành viên khác, ẩn tất cả các bài viết của họ và ngăn chặn gửi bài trong tương lai, điền 0 để tắt." + notify_mods_when_user_blocked: "Nếu một thành viên được khóa tự động, gửi tin nhắn đến tất cả các điều hành viên." + flag_sockpuppets: "Nếu thành viên mới trả lời chủ đề có cùng địa chỉ IP với thành viên mới tạo chủ đề, đánh dấu các bài viết của họ là spam tiềm năng." + traditional_markdown_linebreaks: "Sử dụng ngắt dòng truyền thống trong Markdown, đòi hỏi hai khoảng trống kế tiếp cho một ngắt dòng." + allow_html_tables: "Cho phép nhập bảng trong Markdown sử dụng các thẻ HTML. TABLE, THEAD, TD, TR, TH sẽ được sử dụng (đòi hỏi thực hiện lại cho các bài viết cũ có chứa bảng)" + post_undo_action_window_mins: "Số phút thành viên được phép làm lại các hành động gần đây với bài viết (like, đánh dấu...)." + must_approve_users: "Quản trị viên phải duyệt tất cả các tài khoản thành viên mới trước khi họ có quyền truy cập website. LƯU Ý: bật tính năng này trên site đang hoạt động sẽ hủy bỏ quyền truy cập đối với các tài khoản thành viên hiện tại!" + pending_users_reminder_delay: "Thông báo cho quản trị viên nếu thành viên mới đã chờ duyệt lâu hơn số giờ được thiết lập ở đây, đặt là -1 để tắt thông báo." ga_tracking_code: "Mã theo dõi Google analytics (ga.js), ví dụu: UA-12345678-9; chi tiết http://google.com/analytics" ga_domain_name: "Tên miền Google analytics (ga.js), ví dụ: mysite.com; chi tiết http://google.com/analytics" ga_universal_tracking_code: "Mã theo dõi Google Universal Analytics (analytics.js) , Ví dụ: UA-12345678-9; chi tiết http://google.com/analytics" @@ -629,6 +719,21 @@ vi: enable_escaped_fragments: "Trả lại tới Google's Ajax-Crawling API nếu không xác định được webcrawler. Xem chi tiết https://support.google.com/webmasters/answer/174992?hl=en" enable_noscript_support: "Cho phép webcrawler search engine chuẩn hỗ trợ bằng thẻ noscript" allow_moderators_to_create_categories: "Cho phép điều hành viên tạo danh mục mới" + cors_origins: "Cho phép CORS (Cross-Origin Requests). Mỗi nguyên gốc phải kèm theo http:// hoặc https://. Biến env DISCOURSE_ENABLE_CORS phải được đặt là 'true' để bật CORS." + use_admin_ip_whitelist: "Admin chỉ có thể đăng nhập nếu có địa chỉ IP được định nghĩa trước trong danh sách Screened IPs (Admin > Logs > Screened Ips)." + top_menu: "Xác định các mục nào hiển thị trong thanh điều hướng trang chủ, và theo thứ tự nào. Ví dụ latest|new|unread|categories|top|read|posted|bookmarks" + post_menu: "Xác định các mục nào hiển thị trên menu bài viết, và theo thứ tự nào. Ví dụ like|edit|flag|delete|share|bookmark|reply" + post_menu_hidden_items: "Các mục được để ẩn theo mặc định trong menu bài viết trừ khi click vào dấu '...' để mở rộng." + share_links: "Xác định các mục nào hiển thị trên hộp thoại chia sẻ, và theo thứ tự nào." + track_external_right_clicks: "Theo dõi các liên kết ngoài được click đúng (vd: mở trong tab mới) bị vô hiệu hóa theo mặc định do Rewrite URL." + site_contact_username: "Tên tài khoản quản trị viên hợp lệ để gửi tất cả các thông báo tự động. Nếu để trống, tài khoản mặc định của hệ thống sẽ được sử dụng." + send_welcome_message: "Gửi tất cả các thành viên mới một thông điệp chào mừng kèm theo hướng dẫn nhanh." + suppress_reply_directly_below: "Không hiện bộ đếm trả lời mở rộng cho bài viết chỉ có duy nhất một trả lời trực tiếp bên dưới bài viết này." + suppress_reply_directly_above: "Không hiện trả lời mở rộng cho bài viết chỉ có duy nhất một trả lời trực tiếp bên trên bài viết này." + suppress_reply_when_quoting: "Không hiện trả lời mở rộng cho bài viết chỉ trích dẫn trả lời." + max_reply_history: "Số tối đa trả lời được mở rộng khi trả lời mở rộng" + experimental_reply_expansion: "Ẩn các trả lời trung gian khi mở rộng trả lời (thử nghiệm)" + topics_per_period_in_top_summary: "Số lượng chủ đề top hiển thị trong tóm tắt các chủ đề top theo mặc định." email_token_valid_hours: "Token quyên mật khẩu / kích hoạt tài khoản có giá trị trong (n) giờ." email_token_grace_period_hours: "Token quyên mật khẩu / kích hoạt tài khoản vẫn còn giá trị (n) giờ sau khi được gia hạn" enable_badges: "Kích hoạt hệ thống huy hiệu" @@ -1018,6 +1123,16 @@ vi: title: "Điều khoản Dịch vụ" privacy_topic: title: "Chính sách Riêng tư" + badges: + long_descriptions: + good_post: | + Huy hiệu này được trao khi câu trả lời nhận được 24 lượt thích. Làm tốt lắm! + good_topic: | + Huy hiệu này được trao khi bài viết nhận được 25 lượt thích. Làm tốt lắm! + great_post: | + Huy hiệu này được trao khi bài viết nhận được 50 lượt thích. Wow! + great_topic: | + Huy hiệu này được trao khi câu trả lời nhận được 50 lượt thích. Wow! admin_login: success: "Gửi mail lỗi" error: "Lỗi!" @@ -1025,4 +1140,8 @@ vi: submit_button: "Gửi email" performance_report: initial_topic_title: Báo cáo hiệu suất website - + time: + <<: *datetime_formats + activemodel: + errors: + <<: *errors diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index eda4df6eb0..08abef9992 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -12,9 +12,9 @@ zh_CN: long_date: "lll" datetime_formats: &datetime_formats formats: - short: "%Y-%m-%d" - short_no_year: "%B %-d" - date_only: "%B %-d, %Y" + short: "%Y 年 %-m 月 %-d 日" + short_no_year: "%-m 月 %-d 日" + date_only: "%Y 年 %-m 月 %-d 日" date: month_names: [null, 一月, 二月, 三月, 四月, 五月, 六月, 七月, 八月, 九月, 十月, 十一月, 十二月] <<: *datetime_formats @@ -1269,7 +1269,7 @@ zh_CN: off_topic: "你的帖子被标记为 **偏离主题**:鉴于当前的主题标题和第一个帖子,社群成员们感觉它不适合处于这个主题中。" inappropriate: "你的帖子被标记为 **不恰当**:社群成员感觉它有冒犯或者侮辱的意味,亦或是它违反了[社群准则](/guidelines)。" spam: "你的帖子被标记为 **广告**:社群成员觉得它是广告,像是在过度地推广着什么,而不是预期中与主题有关的内容。" - notify_moderators: "你的帖子被标记为 **需要版主关注**:社群成员感觉帖子需要职员的人工干预。" + notify_moderators: "你的帖子被标记为 **需要版主关注**:社群成员认为帖子需要职员介入。" flags_dispositions: agreed: "感谢通知我们。我们认为这是一个问题,并且我们正在了解情况。" agreed_and_deleted: "感谢通知我们。我们认为这是一个问题,并且我们已经删除了帖子。" diff --git a/plugins/poll/config/locales/client.ja.yml b/plugins/poll/config/locales/client.ja.yml index 91f9906891..a3b3f1bbb4 100644 --- a/plugins/poll/config/locales/client.ja.yml +++ b/plugins/poll/config/locales/client.ja.yml @@ -15,6 +15,12 @@ ja: average_rating: "平均評価: %{average}." multiple: help: + at_least_min_options: + other: "少なくとも %{count} 個のオプションを選んでください。" + up_to_max_options: + other: "%{count} 個のオプションまで選択することができます。" + x_options: + other: "%{count} 個のオプションを選択してください。" between_min_and_max_options: "%{min}%{max} のオプションから選択することができます。" cast-votes: title: "投票する" diff --git a/plugins/poll/config/locales/client.vi.yml b/plugins/poll/config/locales/client.vi.yml index e0fec57092..30d6e6a699 100644 --- a/plugins/poll/config/locales/client.vi.yml +++ b/plugins/poll/config/locales/client.vi.yml @@ -1,13 +1,26 @@ +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + vi: js: poll: voters: - other: "người bình chọn" + other: "Người bầu chọn" total_votes: other: "tổng số bình chọn" average_rating: "Đánh giá trung bình: %{average}." multiple: help: + at_least_min_options: + other: "Bạn phải chọn ít nhất %{count} tùy chọn." + up_to_max_options: + other: "Bạn có thể chọn lên tới %{count} tùy chọn." + x_options: + other: "Bạn phải chọn %{count} tùy chọn." between_min_and_max_options: "Bạn có thể chọn giữa %{min}%{max}." cast-votes: title: "Bỏ phiếu của bạn" @@ -28,4 +41,3 @@ vi: confirm: "Bạn có chắc chắn muốn đóng bình chọn này?" error_while_toggling_status: "Có lỗi trong khi chuyển đổi qua lại các trạng thái của bình chọn này." error_while_casting_votes: "Có lỗi trong khi tạo mãu bầu chọn của bạn" - diff --git a/plugins/poll/config/locales/server.ja.yml b/plugins/poll/config/locales/server.ja.yml index cb3b38ce9e..6353abfeb5 100644 --- a/plugins/poll/config/locales/server.ja.yml +++ b/plugins/poll/config/locales/server.ja.yml @@ -16,6 +16,8 @@ ja: named_poll_must_have_at_least_2_options: "投票 %{name} は少なくとも2つのオプションが必要です。" default_poll_must_have_less_options: other: "投票は%{count}オプション以下でなければいけません。" + named_poll_must_have_less_options: + other: "投票 %{name} は%{count}オプション以下でなければいけません。" default_poll_must_have_different_options: "投票は異なるオプションが必要です。" named_poll_must_have_different_options: "投票 %{name} は異なるオプションが必要です。" default_poll_with_multiple_choices_has_invalid_parameters: "複数の選択肢をもつ投票に無効なパラメータがあります。" @@ -31,3 +33,5 @@ ja: poll_must_be_open_to_vote: "投票するにはオープンになっている必要があります。" topic_must_be_open_to_toggle_status: "状態を切り替えるには、トピックがオープンになっている必要があります。" only_staff_or_op_can_toggle_status: "スタッフやオリジナル投稿者のみが投票状態を切り替えることができます。" + email: + link_to_poll: "クリックして投票を表示。" diff --git a/plugins/poll/config/locales/server.vi.yml b/plugins/poll/config/locales/server.vi.yml index e5c0d30eec..8cd71609a2 100644 --- a/plugins/poll/config/locales/server.vi.yml +++ b/plugins/poll/config/locales/server.vi.yml @@ -1,3 +1,10 @@ +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + vi: site_settings: poll_enabled: "Cho phép người dùng tạo các cuộc thăm dò?" @@ -7,6 +14,10 @@ vi: multiple_polls_with_same_name: "Có nhiều cuộc thăm dò có cùng tên: %{name}. Sử dụng thuộc tính 'name' để xác định cuộc thăm dò của bạn." default_poll_must_have_at_least_2_options: "Thăm dò ý kiến ​​phải có ít nhất 2 lựa chọn." named_poll_must_have_at_least_2_options: "Thăm dò có tên %{name} phải có ít nhất 2 lựa chọn." + default_poll_must_have_less_options: + other: "Thăm dò phải có ít hơn %{count} lựa chọn." + named_poll_must_have_less_options: + other: "Thăm dò %{name} phải có ít hơn %{count} tùy chọn." default_poll_must_have_different_options: "Thăm dò ý kiến ​​phải có các tùy chọn khác nhau." named_poll_must_have_different_options: "Thăm dò %{name} ​​phải có các tùy chọn khác nhau." default_poll_with_multiple_choices_has_invalid_parameters: "Thăm dò ý kiến ​​với nhiều sự lựa chọn có các tham số không hợp lệ." @@ -22,4 +33,5 @@ vi: poll_must_be_open_to_vote: "Thăm dò ý kiến ​​phải được mở để bầu chọn." topic_must_be_open_to_toggle_status: "Các chủ đề phải được mở để chuyển trạng thái." only_staff_or_op_can_toggle_status: "Chỉ có một BQT hoặc các người đăng bài có thể chuyển đổi một trạng thái thăm dò ý kiến" - + email: + link_to_poll: "Nhấn để hiển thị." diff --git a/public/403.vi.html b/public/403.vi.html index 09a5aa396b..20f3770528 100644 --- a/public/403.vi.html +++ b/public/403.vi.html @@ -24,4 +24,3 @@
      - diff --git a/public/422.vi.html b/public/422.vi.html index 483475327c..082a63b385 100644 --- a/public/422.vi.html +++ b/public/422.vi.html @@ -23,4 +23,3 @@
      - diff --git a/public/500.vi.html b/public/500.vi.html index 831451072a..79b74efd3f 100644 --- a/public/500.vi.html +++ b/public/500.vi.html @@ -10,4 +10,3 @@

      Không cần tiến hành bất cứ hành động nào. Tuy nhiên, nếu lỗi này vẫn tiếp tục, bạn có thể cung cấp thêm thông tin chi tiết bao gồm các bước để tái tạo lỗi hoặc tạo một thảo luận trên meta category.

      - diff --git a/public/503.vi.html b/public/503.vi.html index 85ca43edc7..a952fd3a5a 100644 --- a/public/503.vi.html +++ b/public/503.vi.html @@ -9,4 +9,3 @@

      Xin lỗi về sự bất tiện này!

      - diff --git a/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml index 6b70e6db43..80b70f96f0 100644 --- a/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml +++ b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.vi.yml @@ -1,6 +1,12 @@ +# encoding: utf-8 +# +# Never edit this file. It will be overwritten when translations are pulled from Transifex. +# +# To work with us on translations, join this project: +# https://www.transifex.com/projects/p/discourse-org/ + vi: site_settings: enable_imgur: "Kích hoạt imgur api để tải file lên, không lưu trữ file tại máy chủ." - imgur_client_id: "Client ID imgur.com của bạn, cần cho chức năng tải ảnh lên. " - imgur_client_secret: "Client secret của bạn tạo imgur.com. Hiện tại không cần để tải ảnh lên, nhưng sẽ có trong tương lai." - + imgur_client_id: "Bạn cần client ID của imgur.com cho chức năng tải file lên." + imgur_client_secret: "Client secret của bạn tại imgur.com chưa cần để tải ảnh lên, nhưng nó sẽ được yêu cầu trong tương lai." From 1b9b68cb5128d94451bcdeacc65fe500733ff88d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 22 Feb 2016 11:27:35 -0500 Subject: [PATCH 140/140] Version bump to v1.5.0.beta11 --- lib/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/version.rb b/lib/version.rb index f7c347c02b..881f27a2c1 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 1 MINOR = 5 TINY = 0 - PRE = 'beta10' + PRE = 'beta11' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end