From d6b8fa3ab2809a107834a6eab9b8e10320b28bb6 Mon Sep 17 00:00:00 2001 From: 28pins Date: Tue, 11 Aug 2026 16:22:47 -0500 Subject: [PATCH 1/4] Add support for GFM-style block-quote alerts with color and SF Symbol icon --- Sources/MarkdownText/Block/BlockQuote+.swift | 31 +- .../MarkdownRenderConfig+Builders.swift | 21 + .../Models/MarkdownRenderConfig.swift | 39 +- .../Models/RenderableDocument.swift | 4 +- .../Resources/Localizable.xcstrings | 2876 ++++++++--------- Sources/MarkdownText/UI/BlockQuoteView.swift | 66 +- .../BlockQuoteViewSnapshotTests.swift | 102 +- .../MarkdownParserTests.swift | 46 + .../MarkdownTextTests/MarkdownTextTests.swift | 4 +- 9 files changed, 1685 insertions(+), 1504 deletions(-) diff --git a/Sources/MarkdownText/Block/BlockQuote+.swift b/Sources/MarkdownText/Block/BlockQuote+.swift index cfe8453..6f82c0f 100644 --- a/Sources/MarkdownText/Block/BlockQuote+.swift +++ b/Sources/MarkdownText/Block/BlockQuote+.swift @@ -10,18 +10,41 @@ import SwiftUI extension BlockQuote: BlockConvertible { var quoteTypes: BlockQuoteType { var finalQuoteTypes = [BlockQuoteType]() + var alertKind: BlockQuoteAlertKind? for child in children { if let inlineContainer = child as? InlineContainer { - // Use our custom extractPlainText method instead of the built-in plainText property - // to properly handle attachment citations - finalQuoteTypes.append(.text(inlineContainer.extractPlainText(removeHeading: false))) + let text = inlineContainer.extractPlainText(removeHeading: false) + if alertKind == nil, let (kind, rest) = Self.parseAlertTag(text) { + alertKind = kind + finalQuoteTypes.append(.text(rest, alertKind)) + } else { + finalQuoteTypes.append(.text(text, alertKind)) + } } else if let blockQuoteContainer = child as? BlockQuote { finalQuoteTypes.append(blockQuoteContainer.quoteTypes) } } - return .nested(finalQuoteTypes) + return .nested(finalQuoteTypes, alertKind) + } + + /// Recognizes a leading GitHub-style `[!KIND]` alert marker, returning the + /// parsed kind and the remaining text with the marker stripped. + static func parseAlertTag(_ text: String) -> (kind: BlockQuoteAlertKind, rest: String)? { + guard text.hasPrefix("[!"), + let close = text.firstIndex(of: "]"), + close > text.index(text.startIndex, offsetBy: 2), + let kind = BlockQuoteAlertKind(rawValue: String(text[text.index(text.startIndex, offsetBy: 2).. MarkdownRenderable { diff --git a/Sources/MarkdownText/Models/MarkdownRenderConfig+Builders.swift b/Sources/MarkdownText/Models/MarkdownRenderConfig+Builders.swift index 019cc3f..3965424 100644 --- a/Sources/MarkdownText/Models/MarkdownRenderConfig+Builders.swift +++ b/Sources/MarkdownText/Models/MarkdownRenderConfig+Builders.swift @@ -256,4 +256,25 @@ extension MarkdownRenderConfig { imageConfig: value ) } + + /// Returns a copy with `blockQuoteAlertStyle` replaced. + public func withBlockQuoteAlertStyle(_ value: [BlockQuoteAlertKind: MarkdownBlockQuoteAlertStyle]) -> MarkdownRenderConfig { + MarkdownRenderConfig( + shouldAnimateText: shouldAnimateText, + blockQuoteStyle: blockQuoteStyle, + headingStyle: headingStyle, + orderedListStyle: orderedListStyle, + paragraphStyle: paragraphStyle, + tableStyle: tableStyle, + inlineStyle: inlineStyle, + textContextMenu: textContextMenu, + citationConfig: citationConfig, + codeBlockConfig: codeBlockConfig, + blockSpacing: blockSpacing, + textSelectionConfig: textSelectionConfig, + thematicBreakColor: thematicBreakColor, + imageConfig: imageConfig, + blockQuoteAlertStyle: value + ) + } } diff --git a/Sources/MarkdownText/Models/MarkdownRenderConfig.swift b/Sources/MarkdownText/Models/MarkdownRenderConfig.swift index 71feecc..0571978 100644 --- a/Sources/MarkdownText/Models/MarkdownRenderConfig.swift +++ b/Sources/MarkdownText/Models/MarkdownRenderConfig.swift @@ -53,6 +53,8 @@ public struct MarkdownRenderConfig: Hashable, Sendable { /// - Important: Image support is **experimental**. The behavior, API, and /// rendering output may change in future releases. Defaults to `.disabled`. public let imageConfig: ImageConfig + /// Per-kind styling applied to block-quote alerts (`> [!NOTE]`). + public let blockQuoteAlertStyle: [BlockQuoteAlertKind: MarkdownBlockQuoteAlertStyle] /// Font and color style for a uniformly-styled run of markdown text. public struct MarkdownTextStyle: Hashable, Sendable { @@ -95,6 +97,30 @@ public struct MarkdownRenderConfig: Hashable, Sendable { } } + /// Styling applied to a block-quote alert of a given `BlockQuoteAlertKind`. + public struct MarkdownBlockQuoteAlertStyle: Hashable, Sendable { + /// Color of the leading accent bar. + public let accentColor: Color + /// Fill color behind the alert content. + public let backgroundColor: Color + /// SF Symbols icon name + public let imageName: String + + /// Create an alert style with the supplied accent and background colors. + public init(accentColor: Color, backgroundColor: Color, imageName: String) { + self.accentColor = accentColor + self.backgroundColor = backgroundColor + self.imageName = imageName + } + + /// A neutral fallback used when a kind is missing from the style map. + public static let `default` = MarkdownBlockQuoteAlertStyle( + accentColor: .blue, + backgroundColor: .blue.opacity(0.08), + imageName: "info.circle" + ) + } + /// Per-level fonts and shared foreground color for markdown headings. public struct MarkdownHeadingTextStyle: Hashable, Sendable { /// Font set for level-1 headings. @@ -222,6 +248,15 @@ public struct MarkdownRenderConfig: Hashable, Sendable { textColor: Color.Theme.Foreground.Primary.Primary750 ) + /// Default per-kind styling for block-quote alerts. + public static let defaultBlockQuoteAlertStyle: [BlockQuoteAlertKind: MarkdownBlockQuoteAlertStyle] = [ + .note: MarkdownBlockQuoteAlertStyle(accentColor: .blue, backgroundColor: .blue.opacity(0.08), imageName: "note.text"), + .tip: MarkdownBlockQuoteAlertStyle(accentColor: .green, backgroundColor: .green.opacity(0.08), imageName: "lightbulb.circle"), + .important: MarkdownBlockQuoteAlertStyle(accentColor: .purple, backgroundColor: .purple.opacity(0.08), imageName: "info.circle"), + .warning: MarkdownBlockQuoteAlertStyle(accentColor: .yellow, backgroundColor: .yellow.opacity(0.08), imageName: "exclamationmark.triangle"), + .caution: MarkdownBlockQuoteAlertStyle(accentColor: .red, backgroundColor: .red.opacity(0.08), imageName: "exclamationmark.octagon") + ] + /// Default styling for `headingStyle`. public static let defaultHeadingStyle = MarkdownHeadingTextStyle( h1Font: Typography.extraLargeTextFonts, @@ -283,7 +318,8 @@ public struct MarkdownRenderConfig: Hashable, Sendable { blockSpacing: CGFloat = MarkdownRenderConfig.defaultBlockSpacing, textSelectionConfig: TextSelectionConfig = .default, thematicBreakColor: Color = MarkdownRenderConfig.defaultThematicBreakColor, - imageConfig: ImageConfig = .disabled + imageConfig: ImageConfig = .disabled, + blockQuoteAlertStyle: [BlockQuoteAlertKind: MarkdownBlockQuoteAlertStyle] = MarkdownRenderConfig.defaultBlockQuoteAlertStyle ) { self.shouldAnimateText = shouldAnimateText self.blockQuoteStyle = blockQuoteStyle @@ -299,6 +335,7 @@ public struct MarkdownRenderConfig: Hashable, Sendable { self.textSelectionConfig = textSelectionConfig self.thematicBreakColor = thematicBreakColor self.imageConfig = imageConfig + self.blockQuoteAlertStyle = blockQuoteAlertStyle } /// The default render config, equivalent to calling `init()` with no diff --git a/Sources/MarkdownText/Models/RenderableDocument.swift b/Sources/MarkdownText/Models/RenderableDocument.swift index 2ef23d7..3be556e 100644 --- a/Sources/MarkdownText/Models/RenderableDocument.swift +++ b/Sources/MarkdownText/Models/RenderableDocument.swift @@ -118,9 +118,9 @@ private extension Array where Element == MarkdownListItem { private extension BlockQuoteType { var plainText: String { switch self { - case .text(let text): + case .text(let text, _): return text - case .nested(let items): + case .nested(let items, _): return items.map { $0.plainText }.joined(separator: "\n") } } diff --git a/Sources/MarkdownText/Resources/Localizable.xcstrings b/Sources/MarkdownText/Resources/Localizable.xcstrings index 4542441..0067d71 100644 --- a/Sources/MarkdownText/Resources/Localizable.xcstrings +++ b/Sources/MarkdownText/Resources/Localizable.xcstrings @@ -1,2134 +1,2134 @@ { - "sourceLanguage": "en", - "strings": { - "Copied": { - "extractionState": "stale", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "تم النسخ" + "sourceLanguage" : "en", + "strings" : { + "a11y_image" : { + "comment" : "Accessibility label for a block image that has no alt text", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Image" + } + } + } + }, + "a11y_image_viewer_close" : { + "comment" : "Accessibility label for the button that dismisses the fullscreen image viewer", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Close" + } + } + } + }, + "a11y_item_position_in_table" : { + "comment" : "a11y understand their position in the table", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الصف %d من %d، العمود %d من %d" } }, - "ca": { - "stringUnit": { - "state": "translated", - "value": "S'ha copiat" + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fila %d de %d, columna %d de %d" } }, - "cs": { - "stringUnit": { - "state": "translated", - "value": "Zkopírováno" + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Řádek %d z(e) %d, sloupec %d z(e) %d" } }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Kopieret" + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Række %d af %d, kolonne %d af %d" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Kopiert" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeile %d von %d, Spalte %d von %d" } }, - "el": { - "stringUnit": { - "state": "translated", - "value": "Αντιγράφηκε" + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Γραμμή %d από %d, Στήλη %d από %d" } }, - "en": { - "stringUnit": { - "state": "new", - "value": "Copied" + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Row %d of %d, Column %d of %d" } }, - "en-GB": { - "stringUnit": { - "state": "translated", - "value": "Copied" + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Row %d of %d, Column %d of %d" } }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Copiado" + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fila %d de %d, columna %d de %d" } }, - "es-MX": { - "stringUnit": { - "state": "translated", - "value": "Copiado" + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fila %d de %d, columna %d de %d" } }, - "fi": { - "stringUnit": { - "state": "translated", - "value": "Kopioitu" + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rivi %d/%d, sarake %d/%d" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Copié" + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ligne %d sur %d, colonne %d sur %d" } }, - "fr-CA": { - "stringUnit": { - "state": "translated", - "value": "Copié" + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ligne %d sur %d, colonne %d sur %d" } }, - "he": { - "stringUnit": { - "state": "translated", - "value": "הועתק" + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שורה %d מתוך %d, עמודה %d מתוך %d" } }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "प्रतिलिपि बनाई गई" + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d की पंक्ति %d, %d का स्तंभ %d" } }, - "hr": { - "stringUnit": { - "state": "translated", - "value": "Kopirano" + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Redak %d od %d, stupac %d od %d" } }, - "hu": { - "stringUnit": { - "state": "translated", - "value": "Másolva" + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d/ %d. sor, %d/ %d. oszlop" } }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Disalin" + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Baris %d dari %d, Kolom %d dari %d" } }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Copiato" + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riga %d di %d, Colonna %d di %d" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コピーされました" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "行 %d/%d、列 %d/%d" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "복사됨" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "행 %d/%d, 열 %d/%d" } }, - "ms": { - "stringUnit": { - "state": "translated", - "value": "Disalin" + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Baris %d daripada %d, Lajur %d daripada %d" } }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kopiert" + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rad %d av %d, kolonne %d av %d" } }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Gekopieerd" + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rij %d van %d, kolom %d van %d" } }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Skopiowano" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiersz %d z %d, Kolumna %d z %d" } }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Copiado" + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linha %d de %d, Coluna %d de %d" } }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Copiado" + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linha %d de %d, Coluna %d de %d" } }, - "ro": { - "stringUnit": { - "state": "translated", - "value": "Copiat" + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rândul %d din %d, coloana %d din %d" } }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Скопировано" + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Строка %d из %d, столбец %d из %d" } }, - "sk": { - "stringUnit": { - "state": "translated", - "value": "Skopírované" + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riadok %d z %d, stĺpec %d z %d" } }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Kopierad" + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rad %d av %d, kolumn %d av %d" } }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คัดลอกแล้ว" + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "แถว %d จาก %d คอลัมน์ %d จาก %d" } }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Kopyalandı" + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satır %d/%d, Sütun %d/%d" } }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Скопійовано" + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рядок %d з %d, стовпець %d з %d" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã sao chép" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hàng %d/%d, Cột %d/%d" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已复制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第 %d 行(共 %d 行),第 %d 列(共 %d 列)" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "已複製" + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "列 %d/%d,欄 %d/%d" } } } }, - "Copy": { - "extractionState": "stale", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "نسخ" + "a11y_open_citation" : { + "comment" : "Accessibility action to open a citation link", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "فتح رابط %@" } }, - "ca": { - "stringUnit": { - "state": "translated", - "value": "Copia" + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obre %@, l'enllaç" } }, - "cs": { - "stringUnit": { - "state": "translated", - "value": "Kopírovat" + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otevřít %@, odkaz" } }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Kopiér" + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Åbn %@, link" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Kopieren" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ öffnen, Link" } }, - "el": { - "stringUnit": { - "state": "translated", - "value": "Αντιγραφή" + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Άνοιγμα %@, σύνδεση" } }, - "en": { - "stringUnit": { - "state": "new", - "value": "Copy" + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open %@, link" } }, - "en-GB": { - "stringUnit": { - "state": "translated", - "value": "Copy" + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open %@, link" } }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Copiar" + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir %@, vínculo" } }, - "es-MX": { - "stringUnit": { - "state": "translated", - "value": "Copiar" + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir %@, vínculo" } }, - "fi": { - "stringUnit": { - "state": "translated", - "value": "Kopioi" + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avaa %@, linkki" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Copier" + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir %@, lien" } }, - "fr-CA": { - "stringUnit": { - "state": "translated", - "value": "Copier" + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir %@, lien" } }, - "he": { - "stringUnit": { - "state": "translated", - "value": "העתקה" + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "פתח את %@, קישור" } }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "प्रतिलिपि बनाएँ" + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ खोलें, लिंक" } }, - "hr": { - "stringUnit": { - "state": "translated", - "value": "Kopiraj" + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otvori %@, veza" } }, - "hu": { - "stringUnit": { - "state": "translated", - "value": "Másolás" + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ megnyitása, hivatkozás" } }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Salin" + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buka %@, tautan" } }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Copia" + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri collegamento %@" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コピー" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を開く、リンク" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "복사" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 열기, 링크" } }, - "ms": { - "stringUnit": { - "state": "translated", - "value": "Salin" + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buka pautan %@," } }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kopier" + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Åpne %@, kobling" } }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Kopiëren" + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ openen, koppeling" } }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kopiuj" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz %@, link" } }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Copiar" + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir %@, link" } }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Copiar" + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir %@, ligação" } }, - "ro": { - "stringUnit": { - "state": "translated", - "value": "Copiați" + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deschideți %@, link" } }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Копировать" + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть %@, ссылка" } }, - "sk": { - "stringUnit": { - "state": "translated", - "value": "Kopírovať" + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otvoriť %@, prepojenie" } }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Kopiera" + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öppna %@, länk" } }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คัดลอก" + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เปิดลิงก์ %@," } }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Kopyala" + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ bağlantısını aç" } }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Копіювати" + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відкрити %@, посилання" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở %@, liên kết" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开 %@,链接" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "複製" + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟 %@,連結" } } } }, - "a11y_image": { - "comment": "Accessibility label for a block image that has no alt text", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Image" + "a11y_task_list_item_checked" : { + "comment" : "Accessibility suffix for a completed task list item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "checked" } } } }, - "a11y_item_position_in_table": { - "comment": "a11y understand their position in the table", - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "الصف %d من %d، العمود %d من %d" + "a11y_task_list_item_unchecked" : { + "comment" : "Accessibility suffix for an incomplete task list item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "unchecked" + } + } + } + }, + "a11y_text_selection_close" : { + "comment" : "Accessibility label for the button that dismisses the text selection modal", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إغلاق" } }, - "ca": { - "stringUnit": { - "state": "translated", - "value": "Fila %d de %d, columna %d de %d" + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tanca" } }, - "cs": { - "stringUnit": { - "state": "translated", - "value": "Řádek %d z(e) %d, sloupec %d z(e) %d" + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zavřít" } }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Række %d af %d, kolonne %d af %d" + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luk" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Zeile %d von %d, Spalte %d von %d" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schließen" } }, - "el": { - "stringUnit": { - "state": "translated", - "value": "Γραμμή %d από %d, Στήλη %d από %d" + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Κλείσιμο" } }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Row %d of %d, Column %d of %d" + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Close" } }, - "en-GB": { - "stringUnit": { - "state": "translated", - "value": "Row %d of %d, Column %d of %d" + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Close" } }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Fila %d de %d, columna %d de %d" + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar" } }, - "es-MX": { - "stringUnit": { - "state": "translated", - "value": "Fila %d de %d, columna %d de %d" + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar" } }, - "fi": { - "stringUnit": { - "state": "translated", - "value": "Rivi %d/%d, sarake %d/%d" + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sulje" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ligne %d sur %d, colonne %d sur %d" + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fermer" } }, - "fr-CA": { - "stringUnit": { - "state": "translated", - "value": "Ligne %d sur %d, colonne %d sur %d" + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fermer" } }, - "he": { - "stringUnit": { - "state": "translated", - "value": "שורה %d מתוך %d, עמודה %d מתוך %d" + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סגירה" } }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "%d की पंक्ति %d, %d का स्तंभ %d" + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बंद करें" } }, - "hr": { - "stringUnit": { - "state": "translated", - "value": "Redak %d od %d, stupac %d od %d" + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zatvori" } }, - "hu": { - "stringUnit": { - "state": "translated", - "value": "%d/ %d. sor, %d/ %d. oszlop" + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bezárás" } }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Baris %d dari %d, Kolom %d dari %d" + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tutup" } }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Riga %d di %d, Colonna %d di %d" + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chiudi" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "行 %d/%d、列 %d/%d" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "閉じる" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "행 %d/%d, 열 %d/%d" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "닫기" } }, - "ms": { - "stringUnit": { - "state": "translated", - "value": "Baris %d daripada %d, Lajur %d daripada %d" + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tutup" } }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Rad %d av %d, kolonne %d av %d" + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lukk" } }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Rij %d van %d, kolom %d van %d" + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sluiten" } }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Wiersz %d z %d, Kolumna %d z %d" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zamknij" } }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Linha %d de %d, Coluna %d de %d" + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fechar" } }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Linha %d de %d, Coluna %d de %d" + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fechar" } }, - "ro": { - "stringUnit": { - "state": "translated", - "value": "Rândul %d din %d, coloana %d din %d" + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Închideți" } }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Строка %d из %d, столбец %d из %d" + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть" } }, - "sk": { - "stringUnit": { - "state": "translated", - "value": "Riadok %d z %d, stĺpec %d z %d" + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zavrieť" } }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Rad %d av %d, kolumn %d av %d" + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stäng" } }, - "th": { - "stringUnit": { - "state": "translated", - "value": "แถว %d จาก %d คอลัมน์ %d จาก %d" + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ปิด" } }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Satır %d/%d, Sütun %d/%d" + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapat" } }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Рядок %d з %d, стовпець %d з %d" + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрити" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hàng %d/%d, Cột %d/%d" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đóng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "第 %d 行(共 %d 行),第 %d 列(共 %d 列)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "列 %d/%d,欄 %d/%d" + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉" } } } }, - "a11y_open_citation": { - "comment": "Accessibility action to open a citation link", - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "فتح رابط %@" + "code_block_copied" : { + "comment" : "Button label shown after a code block has been copied to clipboard", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اكتمل النسخ" } }, - "ca": { - "stringUnit": { - "state": "translated", - "value": "Obre %@, l'enllaç" + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "S'ha copiat" } }, - "cs": { - "stringUnit": { - "state": "translated", - "value": "Otevřít %@, odkaz" + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zkopírováno" } }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Åbn %@, link" + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopieret" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "%@ öffnen, Link" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiert" } }, - "el": { - "stringUnit": { - "state": "translated", - "value": "Άνοιγμα %@, σύνδεση" + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αντιγράφηκε" } }, - "en": { - "stringUnit": { - "state": "new", - "value": "Open %@, link" + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copied" } }, - "en-GB": { - "stringUnit": { - "state": "translated", - "value": "Open %@, link" + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copied" } }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Abrir %@, vínculo" + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado" } }, - "es-MX": { - "stringUnit": { - "state": "translated", - "value": "Abrir %@, vínculo" + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado" } }, - "fi": { - "stringUnit": { - "state": "translated", - "value": "Avaa %@, linkki" + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopioitu" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ouvrir %@, lien" + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copié" } }, - "fr-CA": { - "stringUnit": { - "state": "translated", - "value": "Ouvrir %@, lien" + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copié" } }, - "he": { - "stringUnit": { - "state": "translated", - "value": "פתח את %@, קישור" + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הועתק" } }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "%@ खोलें, लिंक" + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रतिलिपि बनाई गई" } }, - "hr": { - "stringUnit": { - "state": "translated", - "value": "Otvori %@, veza" + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopirano" } }, - "hu": { - "stringUnit": { - "state": "translated", - "value": "%@ megnyitása, hivatkozás" + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Másolva" } }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Buka %@, tautan" + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disalin" } }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Apri collegamento %@" + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiato" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "%@を開く、リンク" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピー済み" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "%@ 열기, 링크" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "복사됨" } }, - "ms": { - "stringUnit": { - "state": "translated", - "value": "Buka pautan %@," + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disalin" } }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Åpne %@, kobling" + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiert" } }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "%@ openen, koppeling" + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gekopieerd" } }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Otwórz %@, link" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skopiowano" } }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Abrir %@, link" + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado" } }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Abrir %@, ligação" + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado" } }, - "ro": { - "stringUnit": { - "state": "translated", - "value": "Deschideți %@, link" + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiat" } }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Открыть %@, ссылка" + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скопировано" } }, - "sk": { - "stringUnit": { - "state": "translated", - "value": "Otvoriť %@, prepojenie" + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skopírované" } }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Öppna %@, länk" + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopierad" } }, - "th": { - "stringUnit": { - "state": "translated", - "value": "เปิดลิงก์ %@," + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "คัดลอกแล้ว" } }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "%@ bağlantısını aç" + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopyalandı" } }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Відкрити %@, посилання" + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скопійовано" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở %@, liên kết" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã sao chép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开 %@,链接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已复制" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "開啟 %@,連結" - } - } - } - }, - "a11y_task_list_item_checked": { - "comment": "Accessibility suffix for a completed task list item", - "extractionState": "extracted_with_value", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "checked" + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已複製" } } } }, - "a11y_task_list_item_unchecked": { - "comment": "Accessibility suffix for an incomplete task list item", - "extractionState": "extracted_with_value", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "unchecked" - } - } - } - }, - "a11y_text_selection_close": { - "comment": "Accessibility label for the button that dismisses the text selection modal", - "extractionState": "extracted_with_value", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Close" + "code_block_copy" : { + "comment" : "Button label to copy a code block to clipboard", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "نسخ" } }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إغلاق" + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copia" } }, - "ca": { - "stringUnit": { - "state": "translated", - "value": "Tanca" + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopírovat" } }, - "cs": { - "stringUnit": { - "state": "translated", - "value": "Zavřít" + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiér" } }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Luk" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopieren" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Schließen" + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αντιγραφή" } }, - "el": { - "stringUnit": { - "state": "translated", - "value": "Κλείσιμο" + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copy" } }, - "en-GB": { - "stringUnit": { - "state": "translated", - "value": "Close" + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy" } }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Cerrar" + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" } }, - "es-MX": { - "stringUnit": { - "state": "translated", - "value": "Cerrar" + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" } }, - "fi": { - "stringUnit": { - "state": "translated", - "value": "Sulje" + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopioi" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Fermer" + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copier" } }, - "fr-CA": { - "stringUnit": { - "state": "translated", - "value": "Fermer" + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copier" } }, - "he": { - "stringUnit": { - "state": "translated", - "value": "סגירה" + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "העתקה" } }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "बंद करें" + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रतिलिपि बनाएँ" } }, - "hr": { - "stringUnit": { - "state": "translated", - "value": "Zatvori" + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiraj" } }, - "hu": { - "stringUnit": { - "state": "translated", - "value": "Bezárás" + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Másolás" } }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Tutup" + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salin" } }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Chiudi" + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copia" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "閉じる" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピー" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "닫기" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "복사" } }, - "ms": { - "stringUnit": { - "state": "translated", - "value": "Tutup" + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salin" } }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Lukk" + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopier" } }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Sluiten" + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiëren" } }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Zamknij" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiuj" } }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Fechar" + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" } }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Fechar" + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" } }, - "ro": { - "stringUnit": { - "state": "translated", - "value": "Închideți" + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiați" } }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Закрыть" + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Копировать" } }, - "sk": { - "stringUnit": { - "state": "translated", - "value": "Zavrieť" + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopírovať" } }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Stäng" + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiera" } }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ปิด" + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "คัดลอก" } }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Kapat" + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopyala" } }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Закрити" + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Копіювати" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đóng" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "关闭" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "關閉" + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" } } } }, - "code_block_copied": { - "comment": "Button label shown after a code block has been copied to clipboard", - "extractionState": "extracted_with_value", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "اكتمل النسخ" + "Copied" : { + "extractionState" : "stale", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم النسخ" } }, - "ca": { - "stringUnit": { - "state": "translated", - "value": "S'ha copiat" + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "S'ha copiat" } }, - "cs": { - "stringUnit": { - "state": "translated", - "value": "Zkopírováno" + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zkopírováno" } }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Kopieret" + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopieret" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Kopiert" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiert" } }, - "el": { - "stringUnit": { - "state": "translated", - "value": "Αντιγράφηκε" + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αντιγράφηκε" } }, - "en": { - "stringUnit": { - "state": "new", - "value": "Copied" + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copied" } }, - "en-GB": { - "stringUnit": { - "state": "translated", - "value": "Copied" + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copied" } }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Copiado" + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado" } }, - "es-MX": { - "stringUnit": { - "state": "translated", - "value": "Copiado" + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado" } }, - "fi": { - "stringUnit": { - "state": "translated", - "value": "Kopioitu" + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopioitu" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Copié" + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copié" } }, - "fr-CA": { - "stringUnit": { - "state": "translated", - "value": "Copié" + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copié" } }, - "he": { - "stringUnit": { - "state": "translated", - "value": "הועתק" + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הועתק" } }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "प्रतिलिपि बनाई गई" + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रतिलिपि बनाई गई" } }, - "hr": { - "stringUnit": { - "state": "translated", - "value": "Kopirano" + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopirano" } }, - "hu": { - "stringUnit": { - "state": "translated", - "value": "Másolva" + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Másolva" } }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Disalin" + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disalin" } }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Copiato" + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiato" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コピー済み" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピーされました" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "복사됨" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "복사됨" } }, - "ms": { - "stringUnit": { - "state": "translated", - "value": "Disalin" + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disalin" } }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kopiert" + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiert" } }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Gekopieerd" + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gekopieerd" } }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Skopiowano" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skopiowano" } }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Copiado" + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado" } }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Copiado" + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado" } }, - "ro": { - "stringUnit": { - "state": "translated", - "value": "Copiat" + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiat" } }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Скопировано" + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скопировано" } }, - "sk": { - "stringUnit": { - "state": "translated", - "value": "Skopírované" + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skopírované" } }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Kopierad" + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopierad" } }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คัดลอกแล้ว" + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "คัดลอกแล้ว" } }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Kopyalandı" + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopyalandı" } }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Скопійовано" + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скопійовано" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã sao chép" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã sao chép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已复制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已复制" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "已複製" + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已複製" } } } }, - "code_block_copy": { - "comment": "Button label to copy a code block to clipboard", - "extractionState": "extracted_with_value", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "نسخ" + "Copy" : { + "extractionState" : "stale", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "نسخ" } }, - "ca": { - "stringUnit": { - "state": "translated", - "value": "Copia" + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copia" } }, - "cs": { - "stringUnit": { - "state": "translated", - "value": "Kopírovat" + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopírovat" } }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Kopiér" + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiér" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Kopieren" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopieren" } }, - "el": { - "stringUnit": { - "state": "translated", - "value": "Αντιγραφή" + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αντιγραφή" } }, - "en": { - "stringUnit": { - "state": "new", - "value": "Copy" + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copy" } }, - "en-GB": { - "stringUnit": { - "state": "translated", - "value": "Copy" + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy" } }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Copiar" + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" } }, - "es-MX": { - "stringUnit": { - "state": "translated", - "value": "Copiar" + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" } }, - "fi": { - "stringUnit": { - "state": "translated", - "value": "Kopioi" + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopioi" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Copier" + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copier" } }, - "fr-CA": { - "stringUnit": { - "state": "translated", - "value": "Copier" + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copier" } }, - "he": { - "stringUnit": { - "state": "translated", - "value": "העתקה" + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "העתקה" } }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "प्रतिलिपि बनाएँ" + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रतिलिपि बनाएँ" } }, - "hr": { - "stringUnit": { - "state": "translated", - "value": "Kopiraj" + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiraj" } }, - "hu": { - "stringUnit": { - "state": "translated", - "value": "Másolás" + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Másolás" } }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Salin" + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salin" } }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Copia" + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copia" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コピー" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピー" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "복사" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "복사" } }, - "ms": { - "stringUnit": { - "state": "translated", - "value": "Salin" + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salin" } }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kopier" + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopier" } }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Kopiëren" + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiëren" } }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kopiuj" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiuj" } }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Copiar" + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" } }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Copiar" + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" } }, - "ro": { - "stringUnit": { - "state": "translated", - "value": "Copiați" + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiați" } }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Копировать" + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Копировать" } }, - "sk": { - "stringUnit": { - "state": "translated", - "value": "Kopírovať" + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopírovať" } }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Kopiera" + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiera" } }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คัดลอก" + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "คัดลอก" } }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Kopyala" + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopyala" } }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Копіювати" + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Копіювати" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "複製" + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" } } } }, - "markdown_list_item": { - "comment": "Accessibility label describing a list item's position, the list length, and the item's text", - "extractionState": "extracted_with_value", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "العنصر %2$d من قائمة تضم %1$d عنصرًا: %3$@" + "markdown_list" : { + "comment" : "Description for a list with some items", + "extractionState" : "stale", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قائمة تتضمن %@ من العناصر" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Liste mit %1$d Elementen, Element %2$d: %3$@" + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Llista amb %@ elements" } }, - "en": { - "stringUnit": { - "state": "new", - "value": "List with %1$d items, item %2$d: %3$@" + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seznam s(e) %@ položkami" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "列表共 %1$d 项,第 %2$d 项:%3$@" - } - } - } - }, - "markdown_list": { - "comment": "Description for a list with some items", - "extractionState": "extracted_with_value", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "قائمة تتضمن %@ من العناصر" + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liste med %@ elementer" } }, - "ca": { - "stringUnit": { - "state": "translated", - "value": "Llista amb %@ elements" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liste mit %@-Elementen" } }, - "cs": { - "stringUnit": { - "state": "translated", - "value": "Seznam s(e) %@ položkami" + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Λίστα με %@ στοιχεία" } }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Liste med %@ elementer" + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "List with %@ items" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Liste mit %@-Elementen" + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "List with %@ items" } }, - "el": { - "stringUnit": { - "state": "translated", - "value": "Λίστα με %@ στοιχεία" + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista con %@ elementos" } }, - "en": { - "stringUnit": { - "state": "new", - "value": "List with %@ items" + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista con %@ elementos" } }, - "en-GB": { - "stringUnit": { - "state": "translated", - "value": "List with %@ items" + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luettelo, jossa on %@ kohdetta" } }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Lista con %@ elementos" + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liste avec %@ éléments" } }, - "es-MX": { - "stringUnit": { - "state": "translated", - "value": "Lista con %@ elementos" + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liste avec %@ éléments" } }, - "fi": { - "stringUnit": { - "state": "translated", - "value": "Luettelo, jossa on %@ kohdetta" + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "רשימה עם %@ פריטים" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Liste avec %@ éléments" + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ आइटम्स वाली सूची" } }, - "fr-CA": { - "stringUnit": { - "state": "translated", - "value": "Liste avec %@ éléments" + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Popis s ovoliko stavki: %@" } }, - "he": { - "stringUnit": { - "state": "translated", - "value": "רשימה עם %@ פריטים" + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista %@ elemmel" } }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "%@ आइटम्स वाली सूची" + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daftar yang berisi %@ item" } }, - "hr": { - "stringUnit": { - "state": "translated", - "value": "Popis s ovoliko stavki: %@" + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elenco con %@ elementi" } }, - "hu": { - "stringUnit": { - "state": "translated", - "value": "Lista %@ elemmel" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 個のアイテムを含むリスト" } }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Daftar yang berisi %@ item" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@개의 항목이 있는 목록" } }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Elenco con %@ elementi" + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Senarai dengan %@ item" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "%@ 個のアイテムを含むリスト" + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liste med %@ elementer" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "%@개의 항목이 있는 목록" + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lijst met %@ items" } }, - "ms": { - "stringUnit": { - "state": "translated", - "value": "Senarai dengan %@ item" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista z %@ elementami" } }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Liste med %@ elementer" + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista com %@ itens" } }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Lijst met %@ items" + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista com %@ itens" } }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Lista z %@ elementami" + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Listă cu %@ elemente" } }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Lista com %@ itens" + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Список из %@ элементов" } }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Lista com %@ itens" + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zoznam s %@ položkami" } }, - "ro": { - "stringUnit": { - "state": "translated", - "value": "Listă cu %@ elemente" + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista med %@ objekt" } }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Список из %@ элементов" + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "รายการที่มี %@ รายการ" } }, - "sk": { - "stringUnit": { - "state": "translated", - "value": "Zoznam s %@ položkami" + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ öğe içeren liste" } }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Lista med %@ objekt" + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Список із такою кількістю елементів: %@" } }, - "th": { - "stringUnit": { - "state": "translated", - "value": "รายการที่มี %@ รายการ" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Danh sách có %@ mục" } }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "%@ öğe içeren liste" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "包含 %@ 项的列表" } }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Список із такою кількістю елементів: %@" + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "包含 %@ 個項目的清單" + } + } + } + }, + "markdown_list_item" : { + "comment" : "Accessibility label describing a list item's position, the list length, and the item's text", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "العنصر %2$d من قائمة تضم %1$d عنصرًا: %3$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Danh sách có %@ mục" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liste mit %1$d Elementen, Element %2$d: %3$@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "包含 %@ 项的列表" + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "List with %1$d items, item %2$d: %3$@" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "包含 %@ 個項目的清單" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列表共 %1$d 项,第 %2$d 项:%3$@" } } } }, - "select_more_text": { - "comment": "Edit-menu action and modal title for selecting the full document text", - "extractionState": "extracted_with_value", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Select more text" + "select_more_text" : { + "comment" : "Edit-menu action and modal title for selecting the full document text", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تحديد المزيد من النص" } }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تحديد المزيد من النص" + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecciona més text" } }, - "ca": { - "stringUnit": { - "state": "translated", - "value": "Selecciona més text" + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vybrat další text" } }, - "cs": { - "stringUnit": { - "state": "translated", - "value": "Vybrat další text" + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vælg mere tekst" } }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Vælg mere tekst" + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weiteren Text auswählen" } }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Weiteren Text auswählen" + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Επιλογή περισσότερου κειμένου" } }, - "el": { - "stringUnit": { - "state": "translated", - "value": "Επιλογή περισσότερου κειμένου" + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Select more text" } }, - "en-GB": { - "stringUnit": { - "state": "translated", - "value": "Select more text" + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select more text" } }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Seleccionar más texto" + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar más texto" } }, - "es-MX": { - "stringUnit": { - "state": "translated", - "value": "Seleccionar más texto" + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar más texto" } }, - "fi": { - "stringUnit": { - "state": "translated", - "value": "Valitse lisää tekstiä" + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valitse lisää tekstiä" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Sélectionner plus de texte" + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sélectionner plus de texte" } }, - "fr-CA": { - "stringUnit": { - "state": "translated", - "value": "Sélectionner plus de texte" + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sélectionner plus de texte" } }, - "he": { - "stringUnit": { - "state": "translated", - "value": "בחר טקסט נוסף" + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בחר טקסט נוסף" } }, - "hi": { - "stringUnit": { - "state": "translated", - "value": "अधिक पाठ का चयन करें" + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अधिक पाठ का चयन करें" } }, - "hr": { - "stringUnit": { - "state": "translated", - "value": "Odaberi više teksta" + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odaberi više teksta" } }, - "hu": { - "stringUnit": { - "state": "translated", - "value": "Több szöveg kijelölése" + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Több szöveg kijelölése" } }, - "id": { - "stringUnit": { - "state": "translated", - "value": "Pilih teks lainnya" + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pilih teks lainnya" } }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Seleziona altro testo" + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleziona altro testo" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "その他のテキストを選択してください" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他のテキストを選択してください" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "더 많은 텍스트 선택" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "더 많은 텍스트 선택" } }, - "ms": { - "stringUnit": { - "state": "translated", - "value": "Pilih lebih banyak teks" + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pilih lebih banyak teks" } }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Velg mer tekst" + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velg mer tekst" } }, - "nl": { - "stringUnit": { - "state": "translated", - "value": "Meer tekst selecteren" + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meer tekst selecteren" } }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Zaznacz więcej tekstu" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaznacz więcej tekstu" } }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Selecionar mais texto" + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecionar mais texto" } }, - "pt-PT": { - "stringUnit": { - "state": "translated", - "value": "Selecionar mais texto" + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecionar mais texto" } }, - "ro": { - "stringUnit": { - "state": "translated", - "value": "Selectați mai mult text" + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selectați mai mult text" } }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Выделить больше текста" + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выделить больше текста" } }, - "sk": { - "stringUnit": { - "state": "translated", - "value": "Vybrať viac textu" + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vybrať viac textu" } }, - "sv": { - "stringUnit": { - "state": "translated", - "value": "Markera mer text" + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Markera mer text" } }, - "th": { - "stringUnit": { - "state": "translated", - "value": "เลือกข้อความเพิ่มเติม" + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เลือกข้อความเพิ่มเติม" } }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Daha fazla metin seçin" + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha fazla metin seçin" } }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Вибрати додатковий текст" + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вибрати додатковий текст" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn thêm văn bản" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn thêm văn bản" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择更多文本" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择更多文本" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "選取更多文字" - } - } - } - }, - "a11y_image_viewer_close": { - "comment": "Accessibility label for the button that dismisses the fullscreen image viewer", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Close" + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選取更多文字" } } } } }, - "version": "1.1" -} + "version" : "1.1" +} \ No newline at end of file diff --git a/Sources/MarkdownText/UI/BlockQuoteView.swift b/Sources/MarkdownText/UI/BlockQuoteView.swift index 608457b..9a17af4 100644 --- a/Sources/MarkdownText/UI/BlockQuoteView.swift +++ b/Sources/MarkdownText/UI/BlockQuoteView.swift @@ -14,31 +14,38 @@ struct BlockQuoteView: View { } var body: some View { - InternalBlockQuoteView(item: item) + if let kind = item.alertKind { + AlertBlockQuoteView(kind: kind) { + InternalBlockQuoteView(item: item, isForAlertQuote: true) + } + } else { + InternalBlockQuoteView(item: item) + } } } private struct InternalBlockQuoteView: View { let item: BlockQuoteType + public var isForAlertQuote: Bool = false var body: some View { HStack(spacing: 8.0) { - if item.isNested { + if item.isNested && !isForAlertQuote { QuoteDivider() .frame(width: 3.0) } VStack(spacing: 12.0) { switch item { - case .text(let text): + case .text(let text, _): HStack { QuoteTextView(text: text) Spacer() } .fixedSize(horizontal: false, vertical: true) - case .nested(let subItems): + case .nested(let subItems, _): ForEach(subItems.indices, id: \.self) { index in InternalBlockQuoteView(item: subItems[index]) .fixedSize(horizontal: false, vertical: true) @@ -74,9 +81,39 @@ struct QuoteDivider: View { } } +private struct AlertBlockQuoteView: View { + @Environment(\.markdownConfig) var config: MarkdownRenderConfig + + let kind: BlockQuoteAlertKind + let content: Content + + init(kind: BlockQuoteAlertKind, @ViewBuilder content: () -> Content) { + self.kind = kind + self.content = content() + } + + var body: some View { + let style = config.blockQuoteAlertStyle[kind] ?? .default + + HStack(spacing: 0) { + RoundedRectangle(cornerRadius: 4.0, style: .continuous) + .foregroundStyle(style.accentColor) + .frame(width: 4.0) + Image(systemName: style.imageName) + .foregroundStyle(style.accentColor) + .padding(.leading, 8.0) + + content + .padding(.leading, 12.0) + } + .padding(8.0) + .background(style.backgroundColor, in: RoundedRectangle(cornerRadius: 8.0, style: .continuous)) + } +} + indirect enum BlockQuoteType: Equatable, Hashable { - case text(String) - case nested([BlockQuoteType]) + case text(String, BlockQuoteAlertKind?) + case nested([BlockQuoteType], BlockQuoteAlertKind?) var isNested: Bool { switch self { @@ -86,4 +123,21 @@ indirect enum BlockQuoteType: Equatable, Hashable { true } } + + var alertKind: BlockQuoteAlertKind? { + switch self { + case .text(_, let kind): + return kind + case .nested(_, let kind): + return kind + } + } +} + +public enum BlockQuoteAlertKind: String, Equatable, Hashable, Sendable, CaseIterable { + case note + case tip + case important + case warning + case caution } diff --git a/Tests/MarkdownTextTests/BlockQuoteViewSnapshotTests.swift b/Tests/MarkdownTextTests/BlockQuoteViewSnapshotTests.swift index f4db8eb..9d464d8 100644 --- a/Tests/MarkdownTextTests/BlockQuoteViewSnapshotTests.swift +++ b/Tests/MarkdownTextTests/BlockQuoteViewSnapshotTests.swift @@ -18,8 +18,8 @@ final class BlockQuoteViewSnapshotTests: SnapshotTestCase { func test_l0_quote() throws { let renderable = BlockQuoteRenderable(quoteType: .nested([ - .text(TestStrings.l0) - ])) + .text(TestStrings.l0, nil) + ], nil)) let view = CanvasView { BlockQuoteView(item: renderable) @@ -30,11 +30,11 @@ final class BlockQuoteViewSnapshotTests: SnapshotTestCase { func test_l0_l1_quote() throws { let renderable = BlockQuoteRenderable(quoteType: .nested([ - .text(TestStrings.l0), + .text(TestStrings.l0, nil), .nested([ - .text(TestStrings.l1) - ]) - ])) + .text(TestStrings.l1, nil) + ], nil) + ], nil)) let view = CanvasView { BlockQuoteView(item: renderable) @@ -45,15 +45,15 @@ final class BlockQuoteViewSnapshotTests: SnapshotTestCase { func test_l0_l1_l0_l1_quote() throws { let renderable = BlockQuoteRenderable(quoteType: .nested([ - .text(TestStrings.l0), + .text(TestStrings.l0, nil), .nested([ - .text(TestStrings.l1) - ]), - .text(TestStrings.l0), + .text(TestStrings.l1, nil) + ], nil), + .text(TestStrings.l0, nil), .nested([ - .text(TestStrings.l1) - ]) - ])) + .text(TestStrings.l1, nil) + ], nil) + ], nil)) let view = CanvasView { BlockQuoteView(item: renderable) @@ -64,12 +64,12 @@ final class BlockQuoteViewSnapshotTests: SnapshotTestCase { func test_l0_l1_l0_quote() throws { let renderable = BlockQuoteRenderable(quoteType: .nested([ - .text(TestStrings.l0), + .text(TestStrings.l0, nil), .nested([ - .text(TestStrings.l1) - ]), - .text(TestStrings.l0) - ])) + .text(TestStrings.l1, nil) + ], nil), + .text(TestStrings.l0, nil) + ], nil)) let view = CanvasView { BlockQuoteView(item: renderable) @@ -80,16 +80,16 @@ final class BlockQuoteViewSnapshotTests: SnapshotTestCase { func test_l0_l1_l2_l1_l0_quote() throws { let renderable = BlockQuoteRenderable(quoteType: .nested([ - .text(TestStrings.l0), + .text(TestStrings.l0, nil), .nested([ - .text(TestStrings.l1), + .text(TestStrings.l1, nil), .nested([ - .text(TestStrings.l2) - ]), - .text(TestStrings.l1) - ]), - .text(TestStrings.l0) - ])) + .text(TestStrings.l2, nil) + ], nil), + .text(TestStrings.l1, nil) + ], nil), + .text(TestStrings.l0, nil) + ], nil)) let view = CanvasView { BlockQuoteView(item: renderable) @@ -100,17 +100,17 @@ final class BlockQuoteViewSnapshotTests: SnapshotTestCase { func test_l0_l1_l2_l3_quote() throws { let renderable = BlockQuoteRenderable(quoteType: .nested([ - .text(TestStrings.l0), + .text(TestStrings.l0, nil), .nested([ - .text(TestStrings.l1), + .text(TestStrings.l1, nil), .nested([ - .text(TestStrings.l2), + .text(TestStrings.l2, nil), .nested([ - .text(TestStrings.l3) - ]) - ]) - ]) - ])) + .text(TestStrings.l3, nil) + ], nil) + ], nil) + ], nil) + ], nil)) let view = CanvasView { BlockQuoteView(item: renderable) @@ -121,28 +121,28 @@ final class BlockQuoteViewSnapshotTests: SnapshotTestCase { func test_l0_l1_l0_l1_l2_l1_l2_l3_l2_l1_l0_quote() throws { let renderable = BlockQuoteRenderable(quoteType: .nested([ - .text(TestStrings.l0), + .text(TestStrings.l0, nil), .nested([ - .text(TestStrings.l1) - ]), - .text(TestStrings.l0), + .text(TestStrings.l1, nil) + ], nil), + .text(TestStrings.l0, nil), .nested([ - .text(TestStrings.l1), + .text(TestStrings.l1, nil), .nested([ - .text(TestStrings.l2) - ]), - .text(TestStrings.l1), + .text(TestStrings.l2, nil) + ], nil), + .text(TestStrings.l1, nil), .nested([ - .text(TestStrings.l2), + .text(TestStrings.l2, nil), .nested([ - .text(TestStrings.l3) - ]), - .text(TestStrings.l2) - ]), - .text(TestStrings.l1) - ]), - .text(TestStrings.l0) - ])) + .text(TestStrings.l3, nil) + ], nil), + .text(TestStrings.l2, nil) + ], nil), + .text(TestStrings.l1, nil) + ], nil), + .text(TestStrings.l0, nil) + ], nil)) let view = CanvasView { BlockQuoteView(item: renderable) diff --git a/Tests/MarkdownTextTests/MarkdownParserTests.swift b/Tests/MarkdownTextTests/MarkdownParserTests.swift index 066f9d0..c355e0b 100644 --- a/Tests/MarkdownTextTests/MarkdownParserTests.swift +++ b/Tests/MarkdownTextTests/MarkdownParserTests.swift @@ -10,6 +10,7 @@ // Created by Jun Yan on 6/13/25. // @testable import SwiftStreamingMarkdown +import Markdown import SwiftUI import XCTest @@ -64,4 +65,49 @@ afterafterafterafterafterafter XCTAssertEqual(parsed.document.childCount, 5) XCTAssertFalse(parsed.speculativeRewritten) } + + /// Tests that a GitHub-style `[!KIND]` block-quote alert is detected and its + /// marker stripped from the rendered text. + func testBlockQuoteAlertParsing() async throws { + let markdown = """ + > [!NOTE] This is a note. + """ + + let document = await parser.parse(text: markdown) + guard let blockQuote = document.children.compactMap({ $0 as? BlockQuote }).first else { + XCTFail("Expected to find a BlockQuote in the parsed markdown") + return + } + + let quoteTypes = blockQuote.quoteTypes + guard case .nested(let subItems, let kind) = quoteTypes else { + XCTFail("Expected nested quote types") + return + } + XCTAssertEqual(kind, .note) + + let text = subItems.flatMap { type -> [String] in + if case .text(let string, _) = type { + return [string] + } + return [] + }.joined(separator: "\n") + XCTAssertTrue(text.contains("This is a note."), "Alert text should be preserved. Got: '\(text)'") + XCTAssertFalse(text.contains("[!NOTE]"), "Alert marker should be stripped. Got: '\(text)'") + } + + /// Tests that a block quote without an alert marker renders as a plain quote. + func testBlockQuoteWithoutAlertMarkerIsPlain() async throws { + let markdown = """ + > Just a plain quote. + """ + + let document = await parser.parse(text: markdown) + guard let blockQuote = document.children.compactMap({ $0 as? BlockQuote }).first else { + XCTFail("Expected to find a BlockQuote in the parsed markdown") + return + } + + XCTAssertNil(blockQuote.quoteTypes.alertKind) + } } diff --git a/Tests/MarkdownTextTests/MarkdownTextTests.swift b/Tests/MarkdownTextTests/MarkdownTextTests.swift index 2f81132..225e2b3 100644 --- a/Tests/MarkdownTextTests/MarkdownTextTests.swift +++ b/Tests/MarkdownTextTests/MarkdownTextTests.swift @@ -193,10 +193,10 @@ final class MarkdownTextTests: XCTestCase { // Extract the text from the quote types var extractedText = "" switch quoteTypes { - case .nested(let types): + case .nested(let types, _): for type in types { switch type { - case .text(let text): + case .text(let text, _): extractedText = text default: break From 20ecd964213266f21f59436f694bd5e979970731 Mon Sep 17 00:00:00 2001 From: 28pins Date: Tue, 11 Aug 2026 19:12:20 -0500 Subject: [PATCH 2/4] Update documentation to reflect GFM alert rendering changes --- .../Resources/Fixtures/kitchen-sink.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/SwiftStreamingMarkdownSample/SwiftStreamingMarkdownSample/Resources/Fixtures/kitchen-sink.md b/Examples/SwiftStreamingMarkdownSample/SwiftStreamingMarkdownSample/Resources/Fixtures/kitchen-sink.md index cac5589..3413e8a 100644 --- a/Examples/SwiftStreamingMarkdownSample/SwiftStreamingMarkdownSample/Resources/Fixtures/kitchen-sink.md +++ b/Examples/SwiftStreamingMarkdownSample/SwiftStreamingMarkdownSample/Resources/Fixtures/kitchen-sink.md @@ -142,7 +142,7 @@ Container directives from Markdown-it and GitHub-style alerts are extension synt ::: > [!NOTE] -> GitHub alert syntax is included as another block quote extension. Unsupported renderers should display it as quoted text. +> GitHub alert syntax is included as another block quote extension. This should display with a colored bar and icon to the left. > [!WARNING] > Alerts with multiple paragraphs should still wrap and stream without corrupting following content. diff --git a/README.md b/README.md index efae63a..347540f 100644 --- a/README.md +++ b/README.md @@ -113,13 +113,13 @@ The renderer targets the subset of CommonMark + GitHub-flavored Markdown that LL - [x] Inline LaTeX math via `\( … \)` - [x] Display LaTeX math via `$$ … $$` - [x] Inline citation pills +- [x] GitHub alerts (`> [!NOTE]`) — rendered as block quotes with icon and color ### Not yet supported - [ ] Footnotes (`[^1]`) - [ ] Highlight (`==text==`), superscript (`^x^`), subscript (`~x~`) - [ ] Raw HTML (`
`, ``, `
-## Mermaid diagram fallback +## Mermaid diagram -Mermaid is intentionally included as an unimplemented markdown feature. Until a diagram renderer exists, this should remain readable as a fenced code block. +Mermaid fences render as interactive diagrams. Configure them via `MermaidConfig`; setting `.disabled` falls back to a readable fenced code block. ```mermaid flowchart TD diff --git a/README.md b/README.md index 347540f..a78a2ed 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ The renderer targets the subset of CommonMark + GitHub-flavored Markdown that LL - [x] `Inline code` - [x] Inline links - [x] Fenced code blocks with language tag +- [x] Mermaid diagrams — rendered as interactive diagrams for `mermaid`-tagged fenced code blocks; theme and opt-out via `MermaidConfig` (`withMermaidConfig`, `.disabled`) - [x] Block quotes (with nested inlines, lists, and citations) - [x] Ordered lists - [x] Unordered lists (with nesting) @@ -121,7 +122,7 @@ The renderer targets the subset of CommonMark + GitHub-flavored Markdown that LL - [ ] Highlight (`==text==`), superscript (`^x^`), subscript (`~x~`) - [ ] Raw HTML (`
`, ``, `