// Copyright 2014 The Gogs Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gitdiff import ( "bufio" "bytes" "context" "fmt" "html" "html/template" "io" "net/url" "os" "os/exec" "regexp" "sort" "strings" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/sergi/go-diff/diffmatchpatch" stdcharset "golang.org/x/net/html/charset" "golang.org/x/text/encoding" "golang.org/x/text/transform" ) // DiffLineType represents the type of a DiffLine. type DiffLineType uint8 // DiffLineType possible values. const ( DiffLinePlain DiffLineType = iota + 1 DiffLineAdd DiffLineDel DiffLineSection ) // DiffFileType represents the type of a DiffFile. type DiffFileType uint8 // DiffFileType possible values. const ( DiffFileAdd DiffFileType = iota + 1 DiffFileChange DiffFileDel DiffFileRename DiffFileCopy ) // DiffLineExpandDirection represents the DiffLineSection expand direction type DiffLineExpandDirection uint8 // DiffLineExpandDirection possible values. const ( DiffLineExpandNone DiffLineExpandDirection = iota + 1 DiffLineExpandSingle DiffLineExpandUpDown DiffLineExpandUp DiffLineExpandDown ) // DiffLine represents a line difference in a DiffSection. type DiffLine struct { LeftIdx int RightIdx int Match int Type DiffLineType Content string Comments []*models.Comment SectionInfo *DiffLineSectionInfo } // DiffLineSectionInfo represents diff line section meta data type DiffLineSectionInfo struct { Path string LastLeftIdx int LastRightIdx int LeftIdx int RightIdx int LeftHunkSize int RightHunkSize int } // BlobExcerptChunkSize represent max lines of excerpt const BlobExcerptChunkSize = 20 // GetType returns the type of a DiffLine. func (d *DiffLine) GetType() int { return int(d.Type) } // CanComment returns whether or not a line can get commented func (d *DiffLine) CanComment() bool { return len(d.Comments) == 0 && d.Type != DiffLineSection } // GetCommentSide returns the comment side of the first comment, if not set returns empty string func (d *DiffLine) GetCommentSide() string { if len(d.Comments) == 0 { return "" } return d.Comments[0].DiffSide() } // GetLineTypeMarker returns the line type marker func (d *DiffLine) GetLineTypeMarker() string { if strings.IndexByte(" +-", d.Content[0]) > -1 { return d.Content[0:1] } return "" } // GetBlobExcerptQuery builds query string to get blob excerpt func (d *DiffLine) GetBlobExcerptQuery() string { query := fmt.Sprintf( "last_left=%d&last_right=%d&"+ "left=%d&right=%d&"+ "left_hunk_size=%d&right_hunk_size=%d&"+ "path=%s", d.SectionInfo.LastLeftIdx, d.SectionInfo.LastRightIdx, d.SectionInfo.LeftIdx, d.SectionInfo.RightIdx, d.SectionInfo.LeftHunkSize, d.SectionInfo.RightHunkSize, url.QueryEscape(d.SectionInfo.Path)) return query } // GetExpandDirection gets DiffLineExpandDirection func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection { if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 { return DiffLineExpandNone } if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 { return DiffLineExpandUp } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 { return DiffLineExpandUpDown } else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 { return DiffLineExpandDown } return DiffLineExpandSingle } func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo { leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line) return &DiffLineSectionInfo{ Path: treePath, LastLeftIdx: lastLeftIdx, LastRightIdx: lastRightIdx, LeftIdx: leftLine, RightIdx: rightLine, LeftHunkSize: leftHunk, RightHunkSize: righHunk, } } // escape a line's content or return <br> needed for copy/paste purposes func getLineContent(content string) string { if len(content) > 0 { return html.EscapeString(content) } return "<br>" } // DiffSection represents a section of a DiffFile. type DiffSection struct { FileName string Name string Lines []*DiffLine } var ( addedCodePrefix = []byte(`<span class="added-code">`) removedCodePrefix = []byte(`<span class="removed-code">`) codeTagSuffix = []byte(`</span>`) ) var unfinishedtagRegex = regexp.MustCompile(`<[^>]*$`) var trailingSpanRegex = regexp.MustCompile(`<span\s*[[:alpha:]="]*?[>]?$`) var entityRegex = regexp.MustCompile(`&[#]*?[0-9[:alpha:]]*$`) // shouldWriteInline represents combinations where we manually write inline changes func shouldWriteInline(diff diffmatchpatch.Diff, lineType DiffLineType) bool { if true && diff.Type == diffmatchpatch.DiffEqual || diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd || diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel { return true } return false } func fixupBrokenSpans(diffs []diffmatchpatch.Diff) []diffmatchpatch.Diff { // Create a new array to store our fixed up blocks fixedup := make([]diffmatchpatch.Diff, 0, len(diffs)) // semantically label some numbers const insert, delete, equal = 0, 1, 2 // record the positions of the last type of each block in the fixedup blocks last := []int{-1, -1, -1} operation := []diffmatchpatch.Operation{diffmatchpatch.DiffInsert, diffmatchpatch.DiffDelete, diffmatchpatch.DiffEqual} // create a writer for insert and deletes toWrite := []strings.Builder{ {}, {}, } // make some flags for insert and delete unfinishedTag := []bool{false, false} unfinishedEnt := []bool{false, false} // store stores the provided text in the writer for the typ store := func(text string, typ int) { (&(toWrite[typ])).WriteString(text) } // hasStored returns true if there is stored content hasStored := func(typ int) bool { return (&toWrite[typ]).Len() > 0 } // stored will return that content stored := func(typ int) string { return (&toWrite[typ]).String() } // empty will empty the stored content empty := func(typ int) { (&toWrite[typ]).Reset() } // pop will remove the stored content appending to a diff block for that typ pop := func(typ int, fixedup []diffmatchpatch.Diff) []diffmatchpatch.Diff { if hasStored(typ) { if last[typ] > last[equal] { fixedup[last[typ]].Text += stored(typ) } else { fixedup = append(fixedup, diffmatchpatch.Diff{ Type: operation[typ], Text: stored(typ), }) } empty(typ) } return fixedup } // Now we walk the provided diffs and check the type of each block in turn for _, diff := range diffs { typ := delete // flag for handling insert or delete typs switch diff.Type { case diffmatchpatch.DiffEqual: // First check if there is anything stored if hasStored(insert) || hasStored(delete) { // There are two reasons for storing content: // 1. Unfinished Entity <- Could be more efficient here by not doing this if we're looking for a tag if unfinishedEnt[insert] || unfinishedEnt[delete] { // we look for a ';' to finish an entity idx := strings.IndexRune(diff.Text, ';') if idx >= 0 { // if we find a ';' store the preceding content to both insert and delete store(diff.Text[:idx+1], insert) store(diff.Text[:idx+1], delete) // and remove it from this block diff.Text = diff.Text[idx+1:] // reset the ent flags unfinishedEnt[insert] = false unfinishedEnt[delete] = false } else { // otherwise store it all on insert and delete store(diff.Text, insert) store(diff.Text, delete) // and empty this block diff.Text = "" } } // 2. Unfinished Tag if unfinishedTag[insert] || unfinishedTag[delete] { // we look for a '>' to finish a tag idx := strings.IndexRune(diff.Text, '>') if idx >= 0 { store(diff.Text[:idx+1], insert) store(diff.Text[:idx+1], delete) diff.Text = diff.Text[idx+1:] unfinishedTag[insert] = false unfinishedTag[delete] = false } else { store(diff.Text, insert) store(diff.Text, delete) diff.Text = "" } } // If we've completed the required tag/entities if !(unfinishedTag[insert] || unfinishedTag[delete] || unfinishedEnt[insert] || unfinishedEnt[delete]) { // pop off the stack fixedup = pop(insert, fixedup) fixedup = pop(delete, fixedup) } // If that has left this diff block empty then shortcut if len(diff.Text) == 0 { continue } } // check if this block ends in an unfinished tag? idx := unfinishedtagRegex.FindStringIndex(diff.Text) if idx != nil { unfinishedTag[insert] = true unfinishedTag[delete] = true } else { // otherwise does it end in an unfinished entity? idx = entityRegex.FindStringIndex(diff.Text) if idx != nil { unfinishedEnt[insert] = true unfinishedEnt[delete] = true } } // If there is an unfinished component if idx != nil { // Store the fragment store(diff.Text[idx[0]:], insert) store(diff.Text[idx[0]:], delete) // and remove it from this block diff.Text = diff.Text[:idx[0]] } // If that hasn't left the block empty if len(diff.Text) > 0 { // store the position of the last equal block and store it in our diffs last[equal] = len(fixedup) fixedup = append(fixedup, diff) } continue case diffmatchpatch.DiffInsert: typ = insert fallthrough case diffmatchpatch.DiffDelete: // First check if there is anything stored for this type if hasStored(typ) { // if there is prepend it to this block, empty the storage and reset our flags diff.Text = stored(typ) + diff.Text empty(typ) unfinishedEnt[typ] = false unfinishedTag[typ] = false } // check if this block ends in an unfinished tag idx := unfinishedtagRegex.FindStringIndex(diff.Text) if idx != nil { unfinishedTag[typ] = true } else { // otherwise does it end in an unfinished entity idx = entityRegex.FindStringIndex(diff.Text) if idx != nil { unfinishedEnt[typ] = true } } // If there is an unfinished component if idx != nil { // Store the fragment store(diff.Text[idx[0]:], typ) // and remove it from this block diff.Text = diff.Text[:idx[0]] } // If that hasn't left the block empty if len(diff.Text) > 0 { // if the last block of this type was after the last equal block if last[typ] > last[equal] { // store this blocks content on that block fixedup[last[typ]].Text += diff.Text } else { // otherwise store the position of the last block of this type and store the block last[typ] = len(fixedup) fixedup = append(fixedup, diff) } } continue } } // pop off any remaining stored content fixedup = pop(insert, fixedup) fixedup = pop(delete, fixedup) return fixedup } func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML { buf := bytes.NewBuffer(nil) match := "" diffs = fixupBrokenSpans(diffs) for _, diff := range diffs { if shouldWriteInline(diff, lineType) { if len(match) > 0 { diff.Text = match + diff.Text match = "" } // Chroma HTML syntax highlighting is done before diffing individual lines in order to maintain consistency. // Since inline changes might split in the middle of a chroma span tag or HTML entity, make we manually put it back together // before writing so we don't try insert added/removed code spans in the middle of one of those // and create broken HTML. This is done by moving incomplete HTML forward until it no longer matches our pattern of // a line ending with an incomplete HTML entity or partial/opening <span>. // EX: // diffs[{Type: dmp.DiffDelete, Text: "language</span><span "}, // {Type: dmp.DiffEqual, Text: "c"}, // {Type: dmp.DiffDelete, Text: "lass="p">}] // After first iteration // diffs[{Type: dmp.DiffDelete, Text: "language</span>"}, //write out // {Type: dmp.DiffEqual, Text: "<span c"}, // {Type: dmp.DiffDelete, Text: "lass="p">,</span>}] // After second iteration // {Type: dmp.DiffEqual, Text: ""}, // write out // {Type: dmp.DiffDelete, Text: "<span class="p">,</span>}] // Final // {Type: dmp.DiffDelete, Text: "<span class="p">,</span>}] // end up writing <span class="removed-code"><span class="p">,</span></span> // Instead of <span class="removed-code">lass="p",</span></span> m := trailingSpanRegex.FindStringSubmatchIndex(diff.Text) if m != nil { match = diff.Text[m[0]:m[1]] diff.Text = strings.TrimSuffix(diff.Text, match) } m = entityRegex.FindStringSubmatchIndex(diff.Text) if m != nil { match = diff.Text[m[0]:m[1]] diff.Text = strings.TrimSuffix(diff.Text, match) } // Print an existing closing span first before opening added/remove-code span so it doesn't unintentionally close it if strings.HasPrefix(diff.Text, "</span>") { buf.WriteString("</span>") diff.Text = strings.TrimPrefix(diff.Text, "</span>") } // If we weren't able to fix it then this should avoid broken HTML by not inserting more spans below // The previous/next diff section will contain the rest of the tag that is missing here if strings.Count(diff.Text, "<") != strings.Count(diff.Text, ">") { buf.WriteString(diff.Text) continue } } switch { case diff.Type == diffmatchpatch.DiffEqual: buf.WriteString(diff.Text) case diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd: buf.Write(addedCodePrefix) buf.WriteString(diff.Text) buf.Write(codeTagSuffix) case diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel: buf.Write(removedCodePrefix) buf.WriteString(diff.Text) buf.Write(codeTagSuffix) } } return template.HTML(buf.Bytes()) } // GetLine gets a specific line by type (add or del) and file line number func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine { var ( difference = 0 addCount = 0 delCount = 0 matchDiffLine *DiffLine ) LOOP: for _, diffLine := range diffSection.Lines { switch diffLine.Type { case DiffLineAdd: addCount++ case DiffLineDel: delCount++ default: if matchDiffLine != nil { break LOOP } difference = diffLine.RightIdx - diffLine.LeftIdx addCount = 0 delCount = 0 } switch lineType { case DiffLineDel: if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference { matchDiffLine = diffLine } case DiffLineAdd: if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference { matchDiffLine = diffLine } } } if addCount == delCount { return matchDiffLine } return nil } var diffMatchPatch = diffmatchpatch.New() func init() { diffMatchPatch.DiffEditCost = 100 } // GetComputedInlineDiffFor computes inline diff for the given line. func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML { if setting.Git.DisableDiffHighlight { return template.HTML(getLineContent(diffLine.Content[1:])) } var ( compareDiffLine *DiffLine diff1 string diff2 string ) // try to find equivalent diff line. ignore, otherwise switch diffLine.Type { case DiffLineSection: return template.HTML(getLineContent(diffLine.Content[1:])) case DiffLineAdd: compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx) if compareDiffLine == nil { return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:])) } diff1 = compareDiffLine.Content diff2 = diffLine.Content case DiffLineDel: compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx) if compareDiffLine == nil { return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:])) } diff1 = diffLine.Content diff2 = compareDiffLine.Content default: if strings.IndexByte(" +-", diffLine.Content[0]) > -1 { return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:])) } return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content)) } diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, diff1[1:]), highlight.Code(diffSection.FileName, diff2[1:]), true) diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord) return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type) } // DiffFile represents a file diff. type DiffFile struct { Name string OldName string Index int Addition, Deletion int Type DiffFileType IsCreated bool IsDeleted bool IsBin bool IsLFSFile bool IsRenamed bool IsAmbiguous bool IsSubmodule bool Sections []*DiffSection IsIncomplete bool IsIncompleteLineTooLong bool IsProtected bool IsGenerated bool IsVendored bool } // GetType returns type of diff file. func (diffFile *DiffFile) GetType() int { return int(diffFile.Type) } // GetTailSection creates a fake DiffLineSection if the last section is not the end of the file func (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommitID, rightCommitID string) *DiffSection { if len(diffFile.Sections) == 0 || diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile { return nil } leftCommit, err := gitRepo.GetCommit(leftCommitID) if err != nil { return nil } rightCommit, err := gitRepo.GetCommit(rightCommitID) if err != nil { return nil } lastSection := diffFile.Sections[len(diffFile.Sections)-1] lastLine := lastSection.Lines[len(lastSection.Lines)-1] leftLineCount := getCommitFileLineCount(leftCommit, diffFile.Name) rightLineCount := getCommitFileLineCount(rightCommit, diffFile.Name) if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx { return nil } tailDiffLine := &DiffLine{ Type: DiffLineSection, Content: " ", SectionInfo: &DiffLineSectionInfo{ Path: diffFile.Name, LastLeftIdx: lastLine.LeftIdx, LastRightIdx: lastLine.RightIdx, LeftIdx: leftLineCount, RightIdx: rightLineCount, }} pre { line-height: 125%; } td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } .highlight .hll { background-color: #ffffcc } .highlight .c { color: #888888 } /* Comment */ .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ .highlight .k { color: #008800; font-weight: bold } /* Keyword */ .highlight .ch { color: #888888 } /* Comment.Hashbang */ .highlight .cm { color: #888888 } /* Comment.Multiline */ .highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */ .highlight .cpf { color: #888888 } /* Comment.PreprocFile */ .highlight .c1 { color: #888888 } /* Comment.Single */ .highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */ .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ .highlight .ge { font-style: italic } /* Generic.Emph */ .highlight .gr { color: #aa0000 } /* Generic.Error */ .highlight .gh { color: #333333 } /* Generic.Heading */ .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ .highlight .go { color: #888888 } /* Generic.Output */ .highlight .gp { color: #555555 } /* Generic.Prompt */ .highlight .gs { font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #666666 } /* Generic.Subheading */ .highlight .gt { color: #aa0000 } /* Generic.Traceback */ .highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #008800 } /* Keyword.Pseudo */ .highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */ .highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */ .highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ .highlight .na { color: #336699 } /* Name.Attribute */ .highlight .nb { color: #003388 } /* Name.Builtin */ .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # # Translators: # mahdi Kereshteh <miki_mika1362@yahoo.com>, 2013. msgid "" msgstr "" "Project-Id-Version: ownCloud\n" "Report-Msgid-Bugs-To: http://bugs.owncloud.org/\n" "POT-Creation-Date: 2013-03-19 00:04+0100\n" "PO-Revision-Date: 2013-03-18 11:40+0000\n" "Last-Translator: miki_mika1362 <miki_mika1362@yahoo.com>\n" "Language-Team: Persian (http://www.transifex.com/projects/p/owncloud/language/fa/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: fa\n" "Plural-Forms: nplurals=1; plural=0;\n" #: js/dropbox.js:7 js/dropbox.js:28 js/google.js:16 js/google.js:34 msgid "Access granted" msgstr "" #: js/dropbox.js:30 js/dropbox.js:96 js/dropbox.js:102 msgid "Error configuring Dropbox storage" msgstr "" #: js/dropbox.js:65 js/google.js:66 msgid "Grant access" msgstr "" #: js/dropbox.js:101 msgid "Please provide a valid Dropbox app key and secret." msgstr "" #: js/google.js:36 js/google.js:93 msgid "Error configuring Google Drive storage" msgstr "" #: lib/config.php:423 msgid "" "<b>Warning:</b> \"smbclient\" is not installed. Mounting of CIFS/SMB shares " "is not possible. Please ask your system administrator to install it." msgstr "" #: lib/config.php:426 msgid "" "<b>Warning:</b> The FTP support in PHP is not enabled or installed. Mounting" " of FTP shares is not possible. Please ask your system administrator to " "install it." msgstr "" #: templates/settings.php:3 msgid "External Storage" msgstr "حافظه خارجی" #: templates/settings.php:9 templates/settings.php:28 msgid "Folder name" msgstr "" #: templates/settings.php:10 msgid "External storage" msgstr "" #: templates/settings.php:11 msgid "Configuration" msgstr "پیکربندی" #: templates/settings.php:12 msgid "Options" msgstr "تنظیمات" #: templates/settings.php:13 msgid "Applicable" msgstr "قابل اجرا" #: templates/settings.php:33 msgid "Add storage" msgstr "" #: templates/settings.php:90 msgid "None set" msgstr "تنظیم نشده" #: templates/settings.php:91 msgid "All Users" msgstr "تمام کاربران" #: templates/settings.php:92 msgid "Groups" msgstr "گروه ها" #: templates/settings.php:100 msgid "Users" msgstr "کاربران" #: templates/settings.php:113 templates/settings.php:114 #: templates/settings.php:149 templates/settings.php:150 msgid "Delete" msgstr "حذف" #: templates/settings.php:129 msgid "Enable User External Storage" msgstr "فعال سازی حافظه خارجی کاربر" #: templates/settings.php:130 msgid "Allow users to mount their own external storage" msgstr "اجازه به کاربران برای متصل کردن منابع ذخیره ی خارجی خودشان" #: templates/settings.php:141 msgid "SSL root certificates" msgstr "" #: templates/settings.php:159 msgid "Import Root Certificate" msgstr ""
# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # # Translators: # mahdi Kereshteh <miki_mika1362@yahoo.com>, 2013. msgid "" msgstr "" "Project-Id-Version: ownCloud\n" "Report-Msgid-Bugs-To: http://bugs.owncloud.org/\n" "POT-Creation-Date: 2013-03-19 00:04+0100\n" "PO-Revision-Date: 2013-03-18 11:40+0000\n" "Last-Translator: miki_mika1362 <miki_mika1362@yahoo.com>\n" "Language-Team: Persian (http://www.transifex.com/projects/p/owncloud/language/fa/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: fa\n" "Plural-Forms: nplurals=1; plural=0;\n" #: js/dropbox.js:7 js/dropbox.js:28 js/google.js:16 js/google.js:34 msgid "Access granted" msgstr "" #: js/dropbox.js:30 js/dropbox.js:96 js/dropbox.js:102 msgid "Error configuring Dropbox storage" msgstr "" #: js/dropbox.js:65 js/google.js:66 msgid "Grant access" msgstr "" #: js/dropbox.js:101 msgid "Please provide a valid Dropbox app key and secret." msgstr "" #: js/google.js:36 js/google.js:93 msgid "Error configuring Google Drive storage" msgstr "" #: lib/config.php:423 msgid "" "<b>Warning:</b> \"smbclient\" is not installed. Mounting of CIFS/SMB shares " "is not possible. Please ask your system administrator to install it." msgstr "" #: lib/config.php:426 msgid "" "<b>Warning:</b> The FTP support in PHP is not enabled or installed. Mounting" " of FTP shares is not possible. Please ask your system administrator to " "install it." msgstr "" #: templates/settings.php:3 msgid "External Storage" msgstr "حافظه خارجی" #: templates/settings.php:9 templates/settings.php:28 msgid "Folder name" msgstr "" #: templates/settings.php:10 msgid "External storage" msgstr "" #: templates/settings.php:11 msgid "Configuration" msgstr "پیکربندی" #: templates/settings.php:12 msgid "Options" msgstr "تنظیمات" #: templates/settings.php:13 msgid "Applicable" msgstr "قابل اجرا" #: templates/settings.php:33 msgid "Add storage" msgstr "" #: templates/settings.php:90 msgid "None set" msgstr "تنظیم نشده" #: templates/settings.php:91 msgid "All Users" msgstr "تمام کاربران" #: templates/settings.php:92 msgid "Groups" msgstr "گروه ها" #: templates/settings.php:100 msgid "Users" msgstr "کاربران" #: templates/settings.php:113 templates/settings.php:114 #: templates/settings.php:149 templates/settings.php:150 msgid "Delete" msgstr "حذف" #: templates/settings.php:129 msgid "Enable User External Storage" msgstr "فعال سازی حافظه خارجی کاربر" #: templates/settings.php:130 msgid "Allow users to mount their own external storage" msgstr "اجازه به کاربران برای متصل کردن منابع ذخیره ی خارجی خودشان" #: templates/settings.php:141 msgid "SSL root certificates" msgstr "" #: templates/settings.php:159 msgid "Import Root Certificate" msgstr ""