commit 99f2e03b6d163c47fac7177fd0ae1e3c7bb293aa Author: Georg Ehrke Date: Sat Sep 5 15:38:57 2015 +0200 initial commit diff --git a/3rdparty/VObject/Cli.php b/3rdparty/VObject/Cli.php new file mode 100644 index 000000000..1be3159ce --- /dev/null +++ b/3rdparty/VObject/Cli.php @@ -0,0 +1,735 @@ +stderr) { + $this->stderr = STDERR; + } + if (!$this->stdout) { + $this->stdout = STDOUT; + } + if (!$this->stdin) { + $this->stdin = STDIN; + } + + // @codeCoverageIgnoreEnd + + + try { + + list($options, $positional) = $this->parseArguments($argv); + + if (isset($options['q'])) { + $this->quiet = true; + } + $this->log($this->colorize('green', "sabre-vobject ") . $this->colorize('yellow', Version::VERSION)); + + foreach($options as $name=>$value) { + + switch($name) { + + case 'q' : + // Already handled earlier. + break; + case 'h' : + case 'help' : + $this->showHelp(); + return 0; + break; + case 'format' : + switch($value) { + + // jcard/jcal documents + case 'jcard' : + case 'jcal' : + + // specific document versions + case 'vcard21' : + case 'vcard30' : + case 'vcard40' : + case 'icalendar20' : + + // specific formats + case 'json' : + case 'mimedir' : + + // icalendar/vcad + case 'icalendar' : + case 'vcard' : + $this->format = $value; + break; + + default : + throw new InvalidArgumentException('Unknown format: ' . $value); + + } + break; + case 'pretty' : + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { + $this->pretty = true; + } + break; + case 'forgiving' : + $this->forgiving = true; + break; + case 'inputformat' : + switch($value) { + // json formats + case 'jcard' : + case 'jcal' : + case 'json' : + $this->inputFormat = 'json'; + break; + + // mimedir formats + case 'mimedir' : + case 'icalendar' : + case 'vcard' : + case 'vcard21' : + case 'vcard30' : + case 'vcard40' : + case 'icalendar20' : + + $this->inputFormat = 'mimedir'; + break; + + default : + throw new InvalidArgumentException('Unknown format: ' . $value); + + } + break; + default : + throw new InvalidArgumentException('Unknown option: ' . $name); + + } + + } + + if (count($positional) === 0) { + $this->showHelp(); + return 1; + } + + if (count($positional) === 1) { + throw new InvalidArgumentException('Inputfile is a required argument'); + } + + if (count($positional) > 3) { + throw new InvalidArgumentException('Too many arguments'); + } + + if (!in_array($positional[0], array('validate','repair','convert','color'))) { + throw new InvalidArgumentException('Uknown command: ' . $positional[0]); + } + + } catch (InvalidArgumentException $e) { + $this->showHelp(); + $this->log('Error: ' . $e->getMessage(),'red'); + return 1; + } + + $command = $positional[0]; + + $this->inputPath = $positional[1]; + $this->outputPath = isset($positional[2])?$positional[2]:'-'; + + if ($this->outputPath !== '-') { + $this->stdout = fopen($this->outputPath,'w'); + } + + if (!$this->inputFormat) { + if (substr($this->inputPath,-5)==='.json') { + $this->inputFormat = 'json'; + } else { + $this->inputFormat = 'mimedir'; + } + } + if (!$this->format) { + if (substr($this->outputPath,-5)==='.json') { + $this->format = 'json'; + } else { + $this->format = 'mimedir'; + } + } + + + $realCode = 0; + + try { + + while($input = $this->readInput()) { + + $returnCode = $this->$command($input); + if ($returnCode!==0) $realCode = $returnCode; + + } + + } catch (EofException $e) { + // end of file + } catch (\Exception $e) { + $this->log('Error: ' . $e->getMessage(),'red'); + return 2; + } + + return $realCode; + + } + + /** + * Shows the help message. + * + * @return void + */ + protected function showHelp() { + + $this->log('Usage:', 'yellow'); + $this->log(" vobject [options] command [arguments]"); + $this->log(''); + $this->log('Options:', 'yellow'); + $this->log($this->colorize('green', ' -q ') . "Don't output anything."); + $this->log($this->colorize('green', ' -help -h ') . "Display this help message."); + $this->log($this->colorize('green', ' --format ') . "Convert to a specific format. Must be one of: vcard, vcard21,"); + $this->log($this->colorize('green', ' --forgiving ') . "Makes the parser less strict."); + $this->log(" vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir."); + $this->log($this->colorize('green', ' --inputformat ') . "If the input format cannot be guessed from the extension, it"); + $this->log(" must be specified here."); + // Only PHP 5.4 and up + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { + $this->log($this->colorize('green', ' --pretty ') . "json pretty-print."); + } + $this->log(''); + $this->log('Commands:', 'yellow'); + $this->log($this->colorize('green', ' validate') . ' source_file Validates a file for correctness.'); + $this->log($this->colorize('green', ' repair') . ' source_file [output_file] Repairs a file.'); + $this->log($this->colorize('green', ' convert') . ' source_file [output_file] Converts a file.'); + $this->log($this->colorize('green', ' color') . ' source_file Colorize a file, useful for debbugging.'); + $this->log(<<log('Examples:', 'yellow'); + $this->log(' vobject convert contact.vcf contact.json'); + $this->log(' vobject convert --format=vcard40 old.vcf new.vcf'); + $this->log(' vobject convert --inputformat=json --format=mimedir - -'); + $this->log(' vobject color calendar.ics'); + $this->log(''); + $this->log('https://github.com/fruux/sabre-vobject','purple'); + + } + + /** + * Validates a VObject file + * + * @param Component $vObj + * @return int + */ + protected function validate($vObj) { + + $returnCode = 0; + + switch($vObj->name) { + case 'VCALENDAR' : + $this->log("iCalendar: " . (string)$vObj->VERSION); + break; + case 'VCARD' : + $this->log("vCard: " . (string)$vObj->VERSION); + break; + } + + $warnings = $vObj->validate(); + if (!count($warnings)) { + $this->log(" No warnings!"); + } else { + + $returnCode = 2; + foreach($warnings as $warn) { + + $this->log(" " . $warn['message']); + + } + + } + + return $returnCode; + + } + + /** + * Repairs a VObject file + * + * @param Component $vObj + * @return int + */ + protected function repair($vObj) { + + $returnCode = 0; + + switch($vObj->name) { + case 'VCALENDAR' : + $this->log("iCalendar: " . (string)$vObj->VERSION); + break; + case 'VCARD' : + $this->log("vCard: " . (string)$vObj->VERSION); + break; + } + + $warnings = $vObj->validate(Node::REPAIR); + if (!count($warnings)) { + $this->log(" No warnings!"); + } else { + foreach($warnings as $warn) { + + $returnCode = 2; + $this->log(" " . $warn['message']); + + } + + } + fwrite($this->stdout, $vObj->serialize()); + + return $returnCode; + + } + + /** + * Converts a vObject file to a new format. + * + * @param Component $vObj + * @return int + */ + protected function convert($vObj) { + + $json = false; + $convertVersion = null; + $forceInput = null; + + switch($this->format) { + case 'json' : + $json = true; + if ($vObj->name === 'VCARD') { + $convertVersion = Document::VCARD40; + } + break; + case 'jcard' : + $json = true; + $forceInput = 'VCARD'; + $convertVersion = Document::VCARD40; + break; + case 'jcal' : + $json = true; + $forceInput = 'VCALENDAR'; + break; + case 'mimedir' : + case 'icalendar' : + case 'icalendar20' : + case 'vcard' : + break; + case 'vcard21' : + $convertVersion = Document::VCARD21; + break; + case 'vcard30' : + $convertVersion = Document::VCARD30; + break; + case 'vcard40' : + $convertVersion = Document::VCARD40; + break; + + } + + if ($forceInput && $vObj->name !== $forceInput) { + throw new \Exception('You cannot convert a ' . strtolower($vObj->name) . ' to ' . $this->format); + } + if ($convertVersion) { + $vObj = $vObj->convert($convertVersion); + } + if ($json) { + $jsonOptions = 0; + if ($this->pretty) { + $jsonOptions = JSON_PRETTY_PRINT; + } + fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); + } else { + fwrite($this->stdout, $vObj->serialize()); + } + + return 0; + + } + + /** + * Colorizes a file + * + * @param Component $vObj + * @return int + */ + protected function color($vObj) { + + fwrite($this->stdout, $this->serializeComponent($vObj)); + + } + + /** + * Returns an ansi color string for a color name. + * + * @param string $color + * @return string + */ + protected function colorize($color, $str, $resetTo = 'default') { + + $colors = array( + 'cyan' => '1;36', + 'red' => '1;31', + 'yellow' => '1;33', + 'blue' => '0;34', + 'green' => '0;32', + 'default' => '0', + 'purple' => '0;35', + ); + return "\033[" . $colors[$color] . 'm' . $str . "\033[".$colors[$resetTo]."m"; + + } + + /** + * Writes out a string in specific color. + * + * @param string $color + * @param string $str + * @return void + */ + protected function cWrite($color, $str) { + + fwrite($this->stdout, $this->colorize($color, $str)); + + } + + protected function serializeComponent(Component $vObj) { + + $this->cWrite('cyan', 'BEGIN'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name . "\n"); + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accomodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * @return int + */ + $sortScore = function($key, $array) { + + if ($array[$key] instanceof Component) { + + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ($array[$key]->name === 'VTIMEZONE') { + $score=300000000; + return $score+$key; + } else { + $score=400000000; + return $score+$key; + } + } else { + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ($array[$key]->name === 'VERSION') { + $score=100000000; + return $score+$key; + } else { + // All other properties + $score=200000000; + return $score+$key; + } + } + } + + }; + + $tmp = $vObj->children; + uksort($vObj->children, function($a, $b) use ($sortScore, $tmp) { + + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + + }); + + foreach($vObj->children as $child) { + if ($child instanceof Component) { + $this->serializeComponent($child); + } else { + $this->serializeProperty($child); + } + } + + $this->cWrite('cyan', 'END'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name . "\n"); + + } + + /** + * Colorizes a property. + * + * @param Property $property + * @return void + */ + protected function serializeProperty(Property $property) { + + if ($property->group) { + $this->cWrite('default', $property->group); + $this->cWrite('red', '.'); + } + + $str = ''; + $this->cWrite('yellow', $property->name); + + foreach($property->parameters as $param) { + + $this->cWrite('red',';'); + $this->cWrite('blue', $param->serialize()); + + } + $this->cWrite('red',':'); + + if ($property instanceof Property\Binary) { + + $this->cWrite('default', 'embedded binary stripped. (' . strlen($property->getValue()) . ' bytes)'); + + } else { + + $parts = $property->getParts(); + $first1 = true; + // Looping through property values + foreach($parts as $part) { + if ($first1) { + $first1 = false; + } else { + $this->cWrite('red', $property->delimiter); + } + $first2 = true; + // Looping through property sub-values + foreach((array)$part as $subPart) { + if ($first2) { + $first2 = false; + } else { + // The sub-value delimiter is always comma + $this->cWrite('red', ','); + } + + $subPart = strtr($subPart, array( + '\\' => $this->colorize('purple', '\\\\', 'green'), + ';' => $this->colorize('purple', '\;', 'green'), + ',' => $this->colorize('purple', '\,', 'green'), + "\n" => $this->colorize('purple', "\\n\n\t", 'green'), + "\r" => "", + )); + + $this->cWrite('green', $subPart); + } + } + + } + $this->cWrite("default", "\n"); + + } + + /** + * Parses the list of arguments. + * + * @param array $argv + * @return void + */ + protected function parseArguments(array $argv) { + + $positional = array(); + $options = array(); + + for($ii=0; $ii < count($argv); $ii++) { + + // Skipping the first argument. + if ($ii===0) continue; + + $v = $argv[$ii]; + + if (substr($v,0,2)==='--') { + // This is a long-form option. + $optionName = substr($v,2); + $optionValue = true; + if (strpos($optionName,'=')) { + list($optionName, $optionValue) = explode('=', $optionName); + } + $options[$optionName] = $optionValue; + } elseif (substr($v,0,1) === '-' && strlen($v)>1) { + // This is a short-form option. + foreach(str_split(substr($v,1)) as $option) { + $options[$option] = true; + } + + } else { + + $positional[] = $v; + + } + + } + + return array($options, $positional); + + } + + protected $parser; + + /** + * Reads the input file + * + * @return Component + */ + protected function readInput() { + + if (!$this->parser) { + if ($this->inputPath!=='-') { + $this->stdin = fopen($this->inputPath,'r'); + } + + if ($this->inputFormat === 'mimedir') { + $this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving?Reader::OPTION_FORGIVING:0)); + } else { + $this->parser = new Parser\Json($this->stdin, ($this->forgiving?Reader::OPTION_FORGIVING:0)); + } + } + + return $this->parser->parse(); + + } + + /** + * Sends a message to STDERR. + * + * @param string $msg + * @return void + */ + protected function log($msg, $color = 'default') { + + if (!$this->quiet) { + if ($color!=='default') { + $msg = $this->colorize($color, $msg); + } + fwrite($this->stderr, $msg . "\n"); + } + + } + +} diff --git a/3rdparty/VObject/Component.php b/3rdparty/VObject/Component.php new file mode 100644 index 000000000..d88f18132 --- /dev/null +++ b/3rdparty/VObject/Component.php @@ -0,0 +1,473 @@ +value syntax, in which case + * properties will automatically be created, or you can just pass a list of + * Component and Property object. + * + * By default, a set of sensible values will be added to the component. For + * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To + * ensure that this does not happen, set $defaults to false. + * + * @param Document $root + * @param string $name such as VCALENDAR, VEVENT. + * @param array $children + * @param bool $defaults + * @return void + */ + public function __construct(Document $root, $name, array $children = array(), $defaults = true) { + + $this->name = strtoupper($name); + $this->root = $root; + + if ($defaults) { + $children = array_merge($this->getDefaults(), $children); + } + + foreach($children as $k=>$child) { + if ($child instanceof Node) { + + // Component or Property + $this->add($child); + } else { + + // Property key=>value + $this->add($k, $child); + } + } + + } + + /** + * Adds a new property or component, and returns the new item. + * + * This method has 3 possible signatures: + * + * add(Component $comp) // Adds a new component + * add(Property $prop) // Adds a new property + * add($name, $value, array $parameters = array()) // Adds a new property + * add($name, array $children = array()) // Adds a new component + * by name. + * + * @return Node + */ + public function add($a1, $a2 = null, $a3 = null) { + + if ($a1 instanceof Node) { + if (!is_null($a2)) { + throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); + } + $a1->parent = $this; + $this->children[] = $a1; + + return $a1; + + } elseif(is_string($a1)) { + + $item = $this->root->create($a1, $a2, $a3); + $item->parent = $this; + $this->children[] = $item; + + return $item; + + } else { + + throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string'); + + } + + } + + /** + * This method removes a component or property from this component. + * + * You can either specify the item by name (like DTSTART), in which case + * all properties/components with that name will be removed, or you can + * pass an instance of a property or component, in which case only that + * exact item will be removed. + * + * The removed item will be returned. In case there were more than 1 items + * removed, only the last one will be returned. + * + * @param mixed $item + * @return void + */ + public function remove($item) { + + if (is_string($item)) { + $children = $this->select($item); + foreach($children as $k=>$child) { + unset($this->children[$k]); + } + return $child; + } else { + foreach($this->children as $k => $child) { + if ($child===$item) { + unset($this->children[$k]); + return $child; + } + } + + throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component'); + + } + + } + + /** + * Returns an iterable list of children + * + * @return array + */ + public function children() { + + return $this->children; + + } + + /** + * This method only returns a list of sub-components. Properties are + * ignored. + * + * @return array + */ + public function getComponents() { + + $result = array(); + foreach($this->children as $child) { + if ($child instanceof Component) { + $result[] = $child; + } + } + + return $result; + + } + + /** + * Returns an array with elements that match the specified name. + * + * This function is also aware of MIME-Directory groups (as they appear in + * vcards). This means that if a property is grouped as "HOME.EMAIL", it + * will also be returned when searching for just "EMAIL". If you want to + * search for a property in a specific group, you can select on the entire + * string ("HOME.EMAIL"). If you want to search on a specific property that + * has not been assigned a group, specify ".EMAIL". + * + * Keys are retained from the 'children' array, which may be confusing in + * certain cases. + * + * @param string $name + * @return array + */ + public function select($name) { + + $group = null; + $name = strtoupper($name); + if (strpos($name,'.')!==false) { + list($group,$name) = explode('.', $name, 2); + } + + $result = array(); + foreach($this->children as $key=>$child) { + + if ( + strtoupper($child->name) === $name && + (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group)) + ) { + + $result[$key] = $child; + + } + } + + reset($result); + return $result; + + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() { + + $str = "BEGIN:" . $this->name . "\r\n"; + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accomodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * @return int + */ + $sortScore = function($key, $array) { + + if ($array[$key] instanceof Component) { + + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ($array[$key]->name === 'VTIMEZONE') { + $score=300000000; + return $score+$key; + } else { + $score=400000000; + return $score+$key; + } + } else { + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ($array[$key]->name === 'VERSION') { + $score=100000000; + return $score+$key; + } else { + // All other properties + $score=200000000; + return $score+$key; + } + } + } + + }; + + $tmp = $this->children; + uksort($this->children, function($a, $b) use ($sortScore, $tmp) { + + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + + }); + + foreach($this->children as $child) $str.=$child->serialize(); + $str.= "END:" . $this->name . "\r\n"; + + return $str; + + } + + /** + * This method returns an array, with the representation as it should be + * encoded in json. This is used to create jCard or jCal documents. + * + * @return array + */ + public function jsonSerialize() { + + $components = array(); + $properties = array(); + + foreach($this->children as $child) { + if ($child instanceof Component) { + $components[] = $child->jsonSerialize(); + } else { + $properties[] = $child->jsonSerialize(); + } + } + + return array( + strtolower($this->name), + $properties, + $components + ); + + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() { + + return array(); + + } + + /* Magic property accessors {{{ */ + + /** + * Using 'get' you will either get a property or component. + * + * If there were no child-elements found with the specified name, + * null is returned. + * + * To use this, this may look something like this: + * + * $event = $calendar->VEVENT; + * + * @param string $name + * @return Property + */ + public function __get($name) { + + $matches = $this->select($name); + if (count($matches)===0) { + return null; + } else { + $firstMatch = current($matches); + /** @var $firstMatch Property */ + $firstMatch->setIterator(new ElementList(array_values($matches))); + return $firstMatch; + } + + } + + /** + * This method checks if a sub-element with the specified name exists. + * + * @param string $name + * @return bool + */ + public function __isset($name) { + + $matches = $this->select($name); + return count($matches)>0; + + } + + /** + * Using the setter method you can add properties or subcomponents + * + * You can either pass a Component, Property + * object, or a string to automatically create a Property. + * + * If the item already exists, it will be removed. If you want to add + * a new item with the same name, always use the add() method. + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) { + + $matches = $this->select($name); + $overWrite = count($matches)?key($matches):null; + + if ($value instanceof Component || $value instanceof Property) { + $value->parent = $this; + if (!is_null($overWrite)) { + $this->children[$overWrite] = $value; + } else { + $this->children[] = $value; + } + } elseif (is_scalar($value) || is_array($value) || is_null($value)) { + $property = $this->root->create($name,$value); + $property->parent = $this; + if (!is_null($overWrite)) { + $this->children[$overWrite] = $property; + } else { + $this->children[] = $property; + } + } else { + throw new \InvalidArgumentException('You must pass a \\Sabre\\VObject\\Component, \\Sabre\\VObject\\Property or scalar type'); + } + + } + + /** + * Removes all properties and components within this component with the + * specified name. + * + * @param string $name + * @return void + */ + public function __unset($name) { + + $matches = $this->select($name); + foreach($matches as $k=>$child) { + + unset($this->children[$k]); + $child->parent = null; + + } + + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + * + * @return void + */ + public function __clone() { + + foreach($this->children as $key=>$child) { + $this->children[$key] = clone $child; + $this->children[$key]->parent = $this; + } + + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, an automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * @return array + */ + public function validate($options = 0) { + + $result = array(); + foreach($this->children as $child) { + $result = array_merge($result, $child->validate($options)); + } + return $result; + + } + +} diff --git a/3rdparty/VObject/Component/VAlarm.php b/3rdparty/VObject/Component/VAlarm.php new file mode 100644 index 000000000..b69d441b4 --- /dev/null +++ b/3rdparty/VObject/Component/VAlarm.php @@ -0,0 +1,108 @@ +TRIGGER; + if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') { + $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); + $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START'; + + $parentComponent = $this->parent; + if ($related === 'START') { + + if ($parentComponent->name === 'VTODO') { + $propName = 'DUE'; + } else { + $propName = 'DTSTART'; + } + + $effectiveTrigger = clone $parentComponent->$propName->getDateTime(); + $effectiveTrigger->add($triggerDuration); + } else { + if ($parentComponent->name === 'VTODO') { + $endProp = 'DUE'; + } elseif ($parentComponent->name === 'VEVENT') { + $endProp = 'DTEND'; + } else { + throw new \LogicException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); + } + + if (isset($parentComponent->$endProp)) { + $effectiveTrigger = clone $parentComponent->$endProp->getDateTime(); + $effectiveTrigger->add($triggerDuration); + } elseif (isset($parentComponent->DURATION)) { + $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); + $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); + $effectiveTrigger->add($duration); + $effectiveTrigger->add($triggerDuration); + } else { + $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); + $effectiveTrigger->add($triggerDuration); + } + } + } else { + $effectiveTrigger = $trigger->getDateTime(); + } + return $effectiveTrigger; + + } + + /** + * Returns true or false depending on if the event falls in the specified + * time-range. This is used for filtering purposes. + * + * The rules used to determine if an event falls within the specified + * time-range is based on the CalDAV specification. + * + * @param \DateTime $start + * @param \DateTime $end + * @return bool + */ + public function isInTimeRange(\DateTime $start, \DateTime $end) { + + $effectiveTrigger = $this->getEffectiveTriggerTime(); + + if (isset($this->DURATION)) { + $duration = VObject\DateTimeParser::parseDuration($this->DURATION); + $repeat = (string)$this->repeat; + if (!$repeat) { + $repeat = 1; + } + + $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat); + + foreach($period as $occurrence) { + + if ($start <= $occurrence && $end > $occurrence) { + return true; + } + } + return false; + } else { + return ($start <= $effectiveTrigger && $end > $effectiveTrigger); + } + + } + +} diff --git a/3rdparty/VObject/Component/VCalendar.php b/3rdparty/VObject/Component/VCalendar.php new file mode 100644 index 000000000..29690e897 --- /dev/null +++ b/3rdparty/VObject/Component/VCalendar.php @@ -0,0 +1,369 @@ + 'Sabre\\VObject\\Component\\VEvent', + 'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy', + 'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal', + 'VTODO' => 'Sabre\\VObject\\Component\\VTodo', + 'VALARM' => 'Sabre\\VObject\\Component\\VAlarm', + ); + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + static public $valueMap = array( + 'BINARY' => 'Sabre\\VObject\\Property\\Binary', + 'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean', + 'CAL-ADDRESS' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', + 'DATE' => 'Sabre\\VObject\\Property\\ICalendar\\Date', + 'DATE-TIME' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', + 'FLOAT' => 'Sabre\\VObject\\Property\\Float', + 'INTEGER' => 'Sabre\\VObject\\Property\\Integer', + 'PERIOD' => 'Sabre\\VObject\\Property\\ICalendar\\Period', + 'RECUR' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', + 'TEXT' => 'Sabre\\VObject\\Property\\Text', + 'TIME' => 'Sabre\\VObject\\Property\\Time', + 'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only. + 'URI' => 'Sabre\\VObject\\Property\\Uri', + 'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset', + ); + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + static public $propertyMap = array( + // Calendar properties + 'CALSCALE' => 'Sabre\\VObject\\Property\\FlatText', + 'METHOD' => 'Sabre\\VObject\\Property\\FlatText', + 'PRODID' => 'Sabre\\VObject\\Property\\FlatText', + 'VERSION' => 'Sabre\\VObject\\Property\\FlatText', + + // Component properties + 'ATTACH' => 'Sabre\\VObject\\Property\\Binary', + 'CATEGORIES' => 'Sabre\\VObject\\Property\\Text', + 'CLASS' => 'Sabre\\VObject\\Property\\FlatText', + 'COMMENT' => 'Sabre\\VObject\\Property\\FlatText', + 'DESCRIPTION' => 'Sabre\\VObject\\Property\\FlatText', + 'GEO' => 'Sabre\\VObject\\Property\\Float', + 'LOCATION' => 'Sabre\\VObject\\Property\\FlatText', + 'PERCENT-COMPLETE' => 'Sabre\\VObject\\Property\\Integer', + 'PRIORITY' => 'Sabre\\VObject\\Property\\Integer', + 'RESOURCES' => 'Sabre\\VObject\\Property\\Text', + 'STATUS' => 'Sabre\\VObject\\Property\\FlatText', + 'SUMMARY' => 'Sabre\\VObject\\Property\\FlatText', + + // Date and Time Component Properties + 'COMPLETED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DTEND' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DUE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DTSTART' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', + 'FREEBUSY' => 'Sabre\\VObject\\Property\\ICalendar\\Period', + 'TRANSP' => 'Sabre\\VObject\\Property\\FlatText', + + // Time Zone Component Properties + 'TZID' => 'Sabre\\VObject\\Property\\FlatText', + 'TZNAME' => 'Sabre\\VObject\\Property\\FlatText', + 'TZOFFSETFROM' => 'Sabre\\VObject\\Property\\UtcOffset', + 'TZOFFSETTO' => 'Sabre\\VObject\\Property\\UtcOffset', + 'TZURL' => 'Sabre\\VObject\\Property\\Uri', + + // Relationship Component Properties + 'ATTENDEE' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', + 'CONTACT' => 'Sabre\\VObject\\Property\\FlatText', + 'ORGANIZER' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', + 'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'RELATED-TO' => 'Sabre\\VObject\\Property\\FlatText', + 'URL' => 'Sabre\\VObject\\Property\\Uri', + 'UID' => 'Sabre\\VObject\\Property\\FlatText', + + // Recurrence Component Properties + 'EXDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'RDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'RRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', + 'EXRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', // Deprecated since rfc5545 + + // Alarm Component Properties + 'ACTION' => 'Sabre\\VObject\\Property\\FlatText', + 'REPEAT' => 'Sabre\\VObject\\Property\\Integer', + 'TRIGGER' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', + + // Change Management Component Properties + 'CREATED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DTSTAMP' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'SEQUENCE' => 'Sabre\\VObject\\Property\\Integer', + + // Request Status + 'REQUEST-STATUS' => 'Sabre\\VObject\\Property\\Text', + + // Additions from draft-daboo-valarm-extensions-04 + 'ALARM-AGENT' => 'Sabre\\VObject\\Property\\Text', + 'ACKNOWLEDGED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'PROXIMITY' => 'Sabre\\VObject\\Property\\Text', + 'DEFAULT-ALARM' => 'Sabre\\VObject\\Property\\Boolean', + + ); + + /** + * Returns the current document type. + * + * @return void + */ + public function getDocumentType() { + + return self::ICALENDAR20; + + } + + /** + * Returns a list of all 'base components'. For instance, if an Event has + * a recurrence rule, and one instance is overridden, the overridden event + * will have the same UID, but will be excluded from this list. + * + * VTIMEZONE components will always be excluded. + * + * @param string $componentName filter by component name + * @return array + */ + public function getBaseComponents($componentName = null) { + + $components = array(); + foreach($this->children as $component) { + + if (!$component instanceof VObject\Component) + continue; + + if (isset($component->{'RECURRENCE-ID'})) + continue; + + if ($componentName && $component->name !== strtoupper($componentName)) + continue; + + if ($component->name === 'VTIMEZONE') + continue; + + $components[] = $component; + + } + + return $components; + + } + + /** + * If this calendar object, has events with recurrence rules, this method + * can be used to expand the event into multiple sub-events. + * + * Each event will be stripped from it's recurrence information, and only + * the instances of the event in the specified timerange will be left + * alone. + * + * In addition, this method will cause timezone information to be stripped, + * and normalized to UTC. + * + * This method will alter the VCalendar. This cannot be reversed. + * + * This functionality is specifically used by the CalDAV standard. It is + * possible for clients to request expand events, if they are rather simple + * clients and do not have the possibility to calculate recurrences. + * + * @param DateTime $start + * @param DateTime $end + * @return void + */ + public function expand(\DateTime $start, \DateTime $end) { + + $newEvents = array(); + + foreach($this->select('VEVENT') as $key=>$vevent) { + + if (isset($vevent->{'RECURRENCE-ID'})) { + unset($this->children[$key]); + continue; + } + + + if (!$vevent->rrule) { + unset($this->children[$key]); + if ($vevent->isInTimeRange($start, $end)) { + $newEvents[] = $vevent; + } + continue; + } + + $uid = (string)$vevent->uid; + if (!$uid) { + throw new \LogicException('Event did not have a UID!'); + } + + $it = new VObject\RecurrenceIterator($this, $vevent->uid); + $it->fastForward($start); + + while($it->valid() && $it->getDTStart() < $end) { + + if ($it->getDTEnd() > $start) { + + $newEvents[] = $it->getEventObject(); + + } + $it->next(); + + } + unset($this->children[$key]); + + } + + // Setting all properties to UTC time. + foreach($newEvents as $newEvent) { + + foreach($newEvent->children as $child) { + if ($child instanceof VObject\Property\ICalendar\DateTime && $child->hasTime()) { + $dt = $child->getDateTimes(); + // We only need to update the first timezone, because + // setDateTimes will match all other timezones to the + // first. + $dt[0]->setTimeZone(new \DateTimeZone('UTC')); + $child->setDateTimes($dt); + } + + } + + $this->add($newEvent); + + } + + // Removing all VTIMEZONE components + unset($this->VTIMEZONE); + + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() { + + return array( + 'VERSION' => '2.0', + 'PRODID' => '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN', + 'CALSCALE' => 'GREGORIAN', + ); + + } + + /** + * Validates the node for correctness. + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @return array + */ + public function validate($options = 0) { + + $warnings = array(); + + $version = $this->select('VERSION'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The VERSION property must appear in the VCALENDAR component exactly 1 time', + 'node' => $this, + ); + } else { + if ((string)$this->VERSION !== '2.0') { + $warnings[] = array( + 'level' => 1, + 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', + 'node' => $this, + ); + } + } + $version = $this->select('PRODID'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The PRODID property must appear in the VCALENDAR component exactly 1 time', + 'node' => $this, + ); + } + if (count($this->CALSCALE) > 1) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The CALSCALE property must not be specified more than once.', + 'node' => $this, + ); + } + if (count($this->METHOD) > 1) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The METHOD property must not be specified more than once.', + 'node' => $this, + ); + } + + $componentsFound = 0; + foreach($this->children as $child) { + if($child instanceof Component) { + $componentsFound++; + } + } + + if ($componentsFound===0) { + $warnings[] = array( + 'level' => 1, + 'message' => 'An iCalendar object must have at least 1 component.', + 'node' => $this, + ); + } + + return array_merge( + $warnings, + parent::validate() + ); + + } + +} + diff --git a/3rdparty/VObject/Component/VCard.php b/3rdparty/VObject/Component/VCard.php new file mode 100644 index 000000000..67946f009 --- /dev/null +++ b/3rdparty/VObject/Component/VCard.php @@ -0,0 +1,352 @@ + 'Sabre\\VObject\\Property\\Binary', + 'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean', + 'CONTENT-ID' => 'Sabre\\VObject\\Property\\FlatText', // vCard 2.1 only + 'DATE' => 'Sabre\\VObject\\Property\\VCard\\Date', + 'DATE-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateTime', + 'DATE-AND-OR-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', // vCard only + 'FLOAT' => 'Sabre\\VObject\\Property\\Float', + 'INTEGER' => 'Sabre\\VObject\\Property\\Integer', + 'LANGUAGE-TAG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag', + 'TIMESTAMP' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp', + 'TEXT' => 'Sabre\\VObject\\Property\\Text', + 'TIME' => 'Sabre\\VObject\\Property\\Time', + 'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only. + 'URI' => 'Sabre\\VObject\\Property\\Uri', + 'URL' => 'Sabre\\VObject\\Property\\Uri', // vCard 2.1 only + 'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset', + ); + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + static public $propertyMap = array( + + // vCard 2.1 properties and up + 'N' => 'Sabre\\VObject\\Property\\Text', + 'FN' => 'Sabre\\VObject\\Property\\FlatText', + 'PHOTO' => 'Sabre\\VObject\\Property\\Binary', // Todo: we should add a class for Binary values. + 'BDAY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', + 'ADR' => 'Sabre\\VObject\\Property\\Text', + 'LABEL' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 + 'TEL' => 'Sabre\\VObject\\Property\\FlatText', + 'EMAIL' => 'Sabre\\VObject\\Property\\FlatText', + 'MAILER' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 + 'GEO' => 'Sabre\\VObject\\Property\\FlatText', + 'TITLE' => 'Sabre\\VObject\\Property\\FlatText', + 'ROLE' => 'Sabre\\VObject\\Property\\FlatText', + 'LOGO' => 'Sabre\\VObject\\Property\\Binary', + // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so + // not supported at the moment + 'ORG' => 'Sabre\\VObject\\Property\\Text', + 'NOTE' => 'Sabre\\VObject\\Property\\FlatText', + 'REV' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp', + 'SOUND' => 'Sabre\\VObject\\Property\\FlatText', + 'URL' => 'Sabre\\VObject\\Property\\Uri', + 'UID' => 'Sabre\\VObject\\Property\\FlatText', + 'VERSION' => 'Sabre\\VObject\\Property\\FlatText', + 'KEY' => 'Sabre\\VObject\\Property\\FlatText', + 'TZ' => 'Sabre\\VObject\\Property\\Text', + + // vCard 3.0 properties + 'CATEGORIES' => 'Sabre\\VObject\\Property\\Text', + 'SORT-STRING' => 'Sabre\\VObject\\Property\\FlatText', + 'PRODID' => 'Sabre\\VObject\\Property\\FlatText', + 'NICKNAME' => 'Sabre\\VObject\\Property\\Text', + 'CLASS' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 + + // rfc2739 properties + 'FBURL' => 'Sabre\\VObject\\Property\\Uri', + 'CAPURI' => 'Sabre\\VObject\\Property\\Uri', + 'CALURI' => 'Sabre\\VObject\\Property\\Uri', + + // rfc4770 properties + 'IMPP' => 'Sabre\\VObject\\Property\\Uri', + + // vCard 4.0 properties + 'XML' => 'Sabre\\VObject\\Property\\FlatText', + 'ANNIVERSARY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', + 'CLIENTPIDMAP' => 'Sabre\\VObject\\Property\\Text', + 'LANG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag', + 'GENDER' => 'Sabre\\VObject\\Property\\Text', + 'KIND' => 'Sabre\\VObject\\Property\\FlatText', + + ); + + /** + * Returns the current document type. + * + * @return void + */ + public function getDocumentType() { + + if (!$this->version) { + $version = (string)$this->VERSION; + switch($version) { + case '2.1' : + $this->version = self::VCARD21; + break; + case '3.0' : + $this->version = self::VCARD30; + break; + case '4.0' : + $this->version = self::VCARD40; + break; + default : + $this->version = self::UNKNOWN; + break; + + } + } + + return $this->version; + + } + + /** + * Converts the document to a different vcard version. + * + * Use one of the VCARD constants for the target. This method will return + * a copy of the vcard in the new version. + * + * At the moment the only supported conversion is from 3.0 to 4.0. + * + * If input and output version are identical, a clone is returned. + * + * @param int $target + * @return VCard + */ + public function convert($target) { + + $converter = new VObject\VCardConverter(); + return $converter->convert($this, $target); + + } + + /** + * VCards with version 2.1, 3.0 and 4.0 are found. + * + * If the VCARD doesn't know its version, 2.1 is assumed. + */ + const DEFAULT_VERSION = self::VCARD21; + + + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * @return array + */ + public function validate($options = 0) { + + $warnings = array(); + + $versionMap = array( + self::VCARD21 => '2.1', + self::VCARD30 => '3.0', + self::VCARD40 => '4.0', + ); + + $version = $this->select('VERSION'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The VERSION property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->VERSION = $versionMap[self::DEFAULT_VERSION]; + } + } else { + $version = (string)$this->VERSION; + if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') { + $warnings[] = array( + 'level' => 1, + 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->VERSION = $versionMap[self::DEFAULT_VERSION]; + } + } + + } + $fn = $this->select('FN'); + if (count($fn)!==1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The FN property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ); + if (($options & self::REPAIR) && count($fn) === 0) { + // We're going to try to see if we can use the contents of the + // N property. + if (isset($this->N)) { + $value = explode(';', (string)$this->N); + if (isset($value[1]) && $value[1]) { + $this->FN = $value[1] . ' ' . $value[0]; + } else { + $this->FN = $value[0]; + } + + // Otherwise, the ORG property may work + } elseif (isset($this->ORG)) { + $this->FN = (string)$this->ORG; + } + + } + } + + return array_merge( + parent::validate($options), + $warnings + ); + + } + + /** + * Returns a preferred field. + * + * VCards can indicate wether a field such as ADR, TEL or EMAIL is + * preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x + * being a number between 1 and 100). + * + * If neither of those parameters are specified, the first is returned, if + * a field with that name does not exist, null is returned. + * + * @param string $fieldName + * @return VObject\Property|null + */ + public function preferred($propertyName) { + + $preferred = null; + $lastPref = 101; + foreach($this->select($propertyName) as $field) { + + $pref = 101; + if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) { + $pref = 1; + } elseif (isset($field['PREF'])) { + $pref = $field['PREF']->getValue(); + } + + if ($pref < $lastPref || is_null($preferred)) { + $preferred = $field; + $lastPref = $pref; + } + + } + return $preferred; + + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() { + + return array( + 'VERSION' => '3.0', + 'PRODID' => '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN', + ); + + } + + /** + * This method returns an array, with the representation as it should be + * encoded in json. This is used to create jCard or jCal documents. + * + * @return array + */ + public function jsonSerialize() { + + // A vcard does not have sub-components, so we're overriding this + // method to remove that array element. + $properties = array(); + + foreach($this->children as $child) { + $properties[] = $child->jsonSerialize(); + } + + return array( + strtolower($this->name), + $properties, + ); + + } + + /** + * Returns the default class for a property name. + * + * @param string $propertyName + * @return string + */ + public function getClassNameForPropertyName($propertyName) { + + $className = parent::getClassNameForPropertyName($propertyName); + // In vCard 4, BINARY no longer exists, and we need URI instead. + + if ($className == 'Sabre\\VObject\\Property\\Binary' && $this->getDocumentType()===self::VCARD40) { + return 'Sabre\\VObject\\Property\\Uri'; + } + return $className; + + } + +} + diff --git a/3rdparty/VObject/Component/VEvent.php b/3rdparty/VObject/Component/VEvent.php new file mode 100644 index 000000000..94c99ea6a --- /dev/null +++ b/3rdparty/VObject/Component/VEvent.php @@ -0,0 +1,71 @@ +RRULE) { + $it = new VObject\RecurrenceIterator($this); + $it->fastForward($start); + + // We fast-forwarded to a spot where the end-time of the + // recurrence instance exceeded the start of the requested + // time-range. + // + // If the starttime of the recurrence did not exceed the + // end of the time range as well, we have a match. + return ($it->getDTStart() < $end && $it->getDTEnd() > $start); + + } + + $effectiveStart = $this->DTSTART->getDateTime(); + if (isset($this->DTEND)) { + + // The DTEND property is considered non inclusive. So for a 3 day + // event in july, dtstart and dtend would have to be July 1st and + // July 4th respectively. + // + // See: + // http://tools.ietf.org/html/rfc5545#page-54 + $effectiveEnd = $this->DTEND->getDateTime(); + + } elseif (isset($this->DURATION)) { + $effectiveEnd = clone $effectiveStart; + $effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) ); + } elseif (!$this->DTSTART->hasTime()) { + $effectiveEnd = clone $effectiveStart; + $effectiveEnd->modify('+1 day'); + } else { + $effectiveEnd = clone $effectiveStart; + } + return ( + ($start <= $effectiveEnd) && ($end > $effectiveStart) + ); + + } + +} diff --git a/3rdparty/VObject/Component/VFreeBusy.php b/3rdparty/VObject/Component/VFreeBusy.php new file mode 100644 index 000000000..b027323a4 --- /dev/null +++ b/3rdparty/VObject/Component/VFreeBusy.php @@ -0,0 +1,68 @@ +select('FREEBUSY') as $freebusy) { + + // We are only interested in FBTYPE=BUSY (the default), + // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. + if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') { + continue; + } + + // The freebusy component can hold more than 1 value, separated by + // commas. + $periods = explode(',', (string)$freebusy); + + foreach($periods as $period) { + // Every period is formatted as [start]/[end]. The start is an + // absolute UTC time, the end may be an absolute UTC time, or + // duration (relative) value. + list($busyStart, $busyEnd) = explode('/', $period); + + $busyStart = VObject\DateTimeParser::parse($busyStart); + $busyEnd = VObject\DateTimeParser::parse($busyEnd); + if ($busyEnd instanceof \DateInterval) { + $tmp = clone $busyStart; + $tmp->add($busyEnd); + $busyEnd = $tmp; + } + + if($start < $busyEnd && $end > $busyStart) { + return false; + } + + } + + } + + return true; + + } + +} + diff --git a/3rdparty/VObject/Component/VJournal.php b/3rdparty/VObject/Component/VJournal.php new file mode 100644 index 000000000..6cef908d1 --- /dev/null +++ b/3rdparty/VObject/Component/VJournal.php @@ -0,0 +1,46 @@ +DTSTART)?$this->DTSTART->getDateTime():null; + if ($dtstart) { + $effectiveEnd = clone $dtstart; + if (!$this->DTSTART->hasTime()) { + $effectiveEnd->modify('+1 day'); + } + + return ($start <= $effectiveEnd && $end > $dtstart); + + } + return false; + + + } + +} diff --git a/3rdparty/VObject/Component/VTodo.php b/3rdparty/VObject/Component/VTodo.php new file mode 100644 index 000000000..401f7400b --- /dev/null +++ b/3rdparty/VObject/Component/VTodo.php @@ -0,0 +1,68 @@ +DTSTART)?$this->DTSTART->getDateTime():null; + $duration = isset($this->DURATION)?VObject\DateTimeParser::parseDuration($this->DURATION):null; + $due = isset($this->DUE)?$this->DUE->getDateTime():null; + $completed = isset($this->COMPLETED)?$this->COMPLETED->getDateTime():null; + $created = isset($this->CREATED)?$this->CREATED->getDateTime():null; + + if ($dtstart) { + if ($duration) { + $effectiveEnd = clone $dtstart; + $effectiveEnd->add($duration); + return $start <= $effectiveEnd && $end > $dtstart; + } elseif ($due) { + return + ($start < $due || $start <= $dtstart) && + ($end > $dtstart || $end >= $due); + } else { + return $start <= $dtstart && $end > $dtstart; + } + } + if ($due) { + return ($start < $due && $end >= $due); + } + if ($completed && $created) { + return + ($start <= $created || $start <= $completed) && + ($end >= $created || $end >= $completed); + } + if ($completed) { + return ($start <= $completed && $end >= $completed); + } + if ($created) { + return ($end > $created); + } + return true; + + } + +} diff --git a/3rdparty/VObject/DateTimeParser.php b/3rdparty/VObject/DateTimeParser.php new file mode 100644 index 000000000..27dfbe5c3 --- /dev/null +++ b/3rdparty/VObject/DateTimeParser.php @@ -0,0 +1,415 @@ +setTimeZone(new \DateTimeZone('UTC')); + return $date; + + } + + /** + * Parses an iCalendar (rfc5545) formatted date and returns a DateTime object + * + * @param string $date + * @return DateTime + */ + static public function parseDate($date) { + + // Format is YYYYMMDD + $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/',$date,$matches); + + if (!$result) { + throw new \LogicException('The supplied iCalendar date value is incorrect: ' . $date); + } + + $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new \DateTimeZone('UTC')); + return $date; + + } + + /** + * Parses an iCalendar (RFC5545) formatted duration value. + * + * This method will either return a DateTimeInterval object, or a string + * suitable for strtotime or DateTime::modify. + * + * @param string $duration + * @param bool $asString + * @return DateInterval|string + */ + static public function parseDuration($duration, $asString = false) { + + $result = preg_match('/^(?P\+|-)?P((?P\d+)W)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?$/', $duration, $matches); + if (!$result) { + throw new \LogicException('The supplied iCalendar duration value is incorrect: ' . $duration); + } + + if (!$asString) { + $invert = false; + if ($matches['plusminus']==='-') { + $invert = true; + } + + + $parts = array( + 'week', + 'day', + 'hour', + 'minute', + 'second', + ); + foreach($parts as $part) { + $matches[$part] = isset($matches[$part])&&$matches[$part]?(int)$matches[$part]:0; + } + + + // We need to re-construct the $duration string, because weeks and + // days are not supported by DateInterval in the same string. + $duration = 'P'; + $days = $matches['day']; + if ($matches['week']) { + $days+=$matches['week']*7; + } + if ($days) + $duration.=$days . 'D'; + + if ($matches['minute'] || $matches['second'] || $matches['hour']) { + $duration.='T'; + + if ($matches['hour']) + $duration.=$matches['hour'].'H'; + + if ($matches['minute']) + $duration.=$matches['minute'].'M'; + + if ($matches['second']) + $duration.=$matches['second'].'S'; + + } + + if ($duration==='P') { + $duration = 'PT0S'; + } + $iv = new \DateInterval($duration); + if ($invert) $iv->invert = true; + + return $iv; + + } + + + + $parts = array( + 'week', + 'day', + 'hour', + 'minute', + 'second', + ); + + $newDur = ''; + foreach($parts as $part) { + if (isset($matches[$part]) && $matches[$part]) { + $newDur.=' '.$matches[$part] . ' ' . $part . 's'; + } + } + + $newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur); + if ($newDur === '+') { $newDur = '+0 seconds'; }; + return $newDur; + + } + + /** + * Parses either a Date or DateTime, or Duration value. + * + * @param string $date + * @param DateTimeZone|string $referenceTZ + * @return DateTime|DateInterval + */ + static public function parse($date, $referenceTZ = null) { + + if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) { + return self::parseDuration($date); + } elseif (strlen($date)===8) { + return self::parseDate($date); + } else { + return self::parseDateTime($date, $referenceTZ); + } + + } + + /** + * This method parses a vCard date and or time value. + * + * This can be used for the DATE, DATE-TIME, TIMESTAMP and + * DATE-AND-OR-TIME value. + * + * This method returns an array, not a DateTime value. + * + * The elements in the array are in the following order: + * year, month, date, hour, minute, second, timezone + * + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the year, etc. + * + * Timezone is either returned as 'Z' or as '+08:00' + * + * For any non-specified values null is returned. + * + * List of date formats that are supported: + * YYYY + * YYYY-MM + * YYYYMMDD + * --MMDD + * ---DD + * + * YYYY-MM-DD + * --MM-DD + * ---DD + * + * List of supported time formats: + * + * HH + * HHMM + * HHMMSS + * -MMSS + * --SS + * + * HH + * HH:MM + * HH:MM:SS + * -MM:SS + * --SS + * + * A full basic-format date-time string looks like : + * 20130603T133901 + * + * A full extended-format date-time string looks like : + * 2013-06-03T13:39:01 + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +1100. + * + * @param string $date + * @return array + */ + static public function parseVCardDateTime($date) { + + $regex = '/^ + (?: # date part + (?: + (?: (?P [0-9]{4}) (?: -)?| --) + (?P [0-9]{2})? + |---) + (?P [0-9]{2})? + )? + (?:T # time part + (?P [0-9]{2} | -) + (?P [0-9]{2} | -)? + (?P [0-9]{2})? + + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + + )? + $/x'; + + + if (!preg_match($regex, $date, $matches)) { + + // Attempting to parse the extended format. + $regex = '/^ + (?: # date part + (?: (?P [0-9]{4}) - | -- ) + (?P [0-9]{2}) - + (?P [0-9]{2}) + )? + (?:T # time part + + (?: (?P [0-9]{2}) : | -) + (?: (?P [0-9]{2}) : | -)? + (?P [0-9]{2})? + + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) + + )? + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + throw new \InvalidArgumentException('Invalid vCard date-time string: ' . $date); + } + + } + $parts = array( + 'year', + 'month', + 'date', + 'hour', + 'minute', + 'second', + 'timezone' + ); + + $result = array(); + foreach($parts as $part) { + + if (empty($matches[$part])) { + $result[$part] = null; + } elseif ($matches[$part] === '-' || $matches[$part] === '--') { + $result[$part] = null; + } else { + $result[$part] = $matches[$part]; + } + + } + + return $result; + + } + + /** + * This method parses a vCard TIME value. + * + * This method returns an array, not a DateTime value. + * + * The elements in the array are in the following order: + * hour, minute, second, timezone + * + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the hour etc. + * + * Timezone is either returned as 'Z' or as '+08:00' + * + * For any non-specified values null is returned. + * + * List of supported time formats: + * + * HH + * HHMM + * HHMMSS + * -MMSS + * --SS + * + * HH + * HH:MM + * HH:MM:SS + * -MM:SS + * --SS + * + * A full basic-format time string looks like : + * 133901 + * + * A full extended-format time string looks like : + * 13:39:01 + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +11:00. + * + * @param string $date + * @return array + */ + static public function parseVCardTime($date) { + + $regex = '/^ + (?P [0-9]{2} | -) + (?P [0-9]{2} | -)? + (?P [0-9]{2})? + + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + $/x'; + + + if (!preg_match($regex, $date, $matches)) { + + // Attempting to parse the extended format. + $regex = '/^ + (?: (?P [0-9]{2}) : | -) + (?: (?P [0-9]{2}) : | -)? + (?P [0-9]{2})? + + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + throw new \InvalidArgumentException('Invalid vCard time string: ' . $date); + } + + } + $parts = array( + 'hour', + 'minute', + 'second', + 'timezone' + ); + + $result = array(); + foreach($parts as $part) { + + if (empty($matches[$part])) { + $result[$part] = null; + } elseif ($matches[$part] === '-') { + $result[$part] = null; + } else { + $result[$part] = $matches[$part]; + } + + } + + return $result; + + } +} diff --git a/3rdparty/VObject/Document.php b/3rdparty/VObject/Document.php new file mode 100644 index 000000000..b669d549f --- /dev/null +++ b/3rdparty/VObject/Document.php @@ -0,0 +1,270 @@ +value syntax, in which case + * properties will automatically be created, or you can just pass a list of + * Component and Property object. + * + * By default, a set of sensible values will be added to the component. For + * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To + * ensure that this does not happen, set $defaults to false. + * + * @param string $name + * @param array $children + * @param bool $defaults + * @return Component + */ + public function createComponent($name, array $children = null, $defaults = true) { + + $name = strtoupper($name); + $class = 'Sabre\\VObject\\Component'; + + if (isset(static::$componentMap[$name])) { + $class=static::$componentMap[$name]; + } + if (is_null($children)) $children = array(); + + if(strpos($class, 'OCA\\Calendar\\') !== 0) { + $class = 'OCA\\Calendar\\' . $class; + } + + return new $class($this, $name, $children, $defaults); + + } + + /** + * Factory method for creating new properties + * + * This method automatically searches for the correct property class, based + * on its name. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param string $name + * @param mixed $value + * @param array $parameters + * @param string $valueType Force a specific valuetype, such as URI or TEXT + * @return Property + */ + public function createProperty($name, $value = null, array $parameters = null, $valueType = null) { + + // If there's a . in the name, it means it's prefixed by a groupname. + if (($i=strpos($name,'.'))!==false) { + $group = substr($name, 0, $i); + $name = strtoupper(substr($name, $i+1)); + } else { + $name = strtoupper($name); + $group = null; + } + + $class = null; + + if ($valueType) { + // The valueType argument comes first to figure out the correct + // class. + $class = $this->getClassNameForPropertyValue($valueType); + } + + if (is_null($class) && isset($parameters['VALUE'])) { + // If a VALUE parameter is supplied, we should use that. + $class = $this->getClassNameForPropertyValue($parameters['VALUE']); + } + if (is_null($class)) { + $class = $this->getClassNameForPropertyName($name); + } + if (is_null($parameters)) $parameters = array(); + + if(strpos($class, 'OCA\\Calendar\\') !== 0) { + $class = 'OCA\\Calendar\\' . $class; + } + + return new $class($this, $name, $value, $parameters, $group); + + } + + /** + * This method returns a full class-name for a value parameter. + * + * For instance, DTSTART may have VALUE=DATE. In that case we will look in + * our valueMap table and return the appropriate class name. + * + * This method returns null if we don't have a specialized class. + * + * @param string $valueParam + * @return void + */ + public function getClassNameForPropertyValue($valueParam) { + + $valueParam = strtoupper($valueParam); + if (isset(static::$valueMap[$valueParam])) { + return static::$valueMap[$valueParam]; + } + + } + + /** + * Returns the default class for a property name. + * + * @param string $propertyName + * @return string + */ + public function getClassNameForPropertyName($propertyName) { + + if (isset(static::$propertyMap[$propertyName])) { + return 'OCA\\Calendar\\' . static::$propertyMap[$propertyName]; + } else { + return 'OCA\\Calendar\\Sabre\\VObject\\Property\\Unknown'; + } + + } + +} diff --git a/3rdparty/VObject/ElementList.php b/3rdparty/VObject/ElementList.php new file mode 100644 index 000000000..2b1d5646d --- /dev/null +++ b/3rdparty/VObject/ElementList.php @@ -0,0 +1,172 @@ +vevent where there's multiple VEVENT objects. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class ElementList implements \Iterator, \Countable, \ArrayAccess { + + /** + * Inner elements + * + * @var array + */ + protected $elements = array(); + + /** + * Creates the element list. + * + * @param array $elements + */ + public function __construct(array $elements) { + + $this->elements = $elements; + + } + + /* {{{ Iterator interface */ + + /** + * Current position + * + * @var int + */ + private $key = 0; + + /** + * Returns current item in iteration + * + * @return Element + */ + public function current() { + + return $this->elements[$this->key]; + + } + + /** + * To the next item in the iterator + * + * @return void + */ + public function next() { + + $this->key++; + + } + + /** + * Returns the current iterator key + * + * @return int + */ + public function key() { + + return $this->key; + + } + + /** + * Returns true if the current position in the iterator is a valid one + * + * @return bool + */ + public function valid() { + + return isset($this->elements[$this->key]); + + } + + /** + * Rewinds the iterator + * + * @return void + */ + public function rewind() { + + $this->key = 0; + + } + + /* }}} */ + + /* {{{ Countable interface */ + + /** + * Returns the number of elements + * + * @return int + */ + public function count() { + + return count($this->elements); + + } + + /* }}} */ + + /* {{{ ArrayAccess Interface */ + + + /** + * Checks if an item exists through ArrayAccess. + * + * @param int $offset + * @return bool + */ + public function offsetExists($offset) { + + return isset($this->elements[$offset]); + + } + + /** + * Gets an item through ArrayAccess. + * + * @param int $offset + * @return mixed + */ + public function offsetGet($offset) { + + return $this->elements[$offset]; + + } + + /** + * Sets an item through ArrayAccess. + * + * @param int $offset + * @param mixed $value + * @return void + */ + public function offsetSet($offset,$value) { + + throw new \LogicException('You can not add new objects to an ElementList'); + + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @return void + */ + public function offsetUnset($offset) { + + throw new \LogicException('You can not remove objects from an ElementList'); + + } + + /* }}} */ + +} diff --git a/3rdparty/VObject/EofException.php b/3rdparty/VObject/EofException.php new file mode 100644 index 000000000..39a045de7 --- /dev/null +++ b/3rdparty/VObject/EofException.php @@ -0,0 +1,13 @@ +setTimeRange($start, $end); + } + + if ($objects) { + $this->setObjects($objects); + } + + } + + /** + * Sets the VCALENDAR object. + * + * If this is set, it will not be generated for you. You are responsible + * for setting things like the METHOD, CALSCALE, VERSION, etc.. + * + * The VFREEBUSY object will be automatically added though. + * + * @param Component $vcalendar + * @return void + */ + public function setBaseObject(Component $vcalendar) { + + $this->baseObject = $vcalendar; + + } + + /** + * Sets the input objects + * + * You must either specify a valendar object as a strong, or as the parse + * Component. + * It's also possible to specify multiple objects as an array. + * + * @param mixed $objects + * @return void + */ + public function setObjects($objects) { + + if (!is_array($objects)) { + $objects = array($objects); + } + + $this->objects = array(); + foreach($objects as $object) { + + if (is_string($object)) { + $this->objects[] = Reader::read($object); + } elseif ($object instanceof Component) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects'); + } + + } + + } + + /** + * Sets the time range + * + * Any freebusy object falling outside of this time range will be ignored. + * + * @param DateTime $start + * @param DateTime $end + * @return void + */ + public function setTimeRange(\DateTime $start = null, \DateTime $end = null) { + + $this->start = $start; + $this->end = $end; + + } + + /** + * Parses the input data and returns a correct VFREEBUSY object, wrapped in + * a VCALENDAR. + * + * @return Component + */ + public function getResult() { + + $busyTimes = array(); + + foreach($this->objects as $object) { + + foreach($object->getBaseComponents() as $component) { + + switch($component->name) { + + case 'VEVENT' : + + $FBTYPE = 'BUSY'; + if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) { + break; + } + if (isset($component->STATUS)) { + $status = strtoupper($component->STATUS); + if ($status==='CANCELLED') { + break; + } + if ($status==='TENTATIVE') { + $FBTYPE = 'BUSY-TENTATIVE'; + } + } + + $times = array(); + + if ($component->RRULE) { + + $iterator = new RecurrenceIterator($object, (string)$component->uid); + if ($this->start) { + $iterator->fastForward($this->start); + } + + $maxRecurrences = 200; + + while($iterator->valid() && --$maxRecurrences) { + + $startTime = $iterator->getDTStart(); + if ($this->end && $startTime > $this->end) { + break; + } + $times[] = array( + $iterator->getDTStart(), + $iterator->getDTEnd(), + ); + + $iterator->next(); + + } + + } else { + + $startTime = $component->DTSTART->getDateTime(); + if ($this->end && $startTime > $this->end) { + break; + } + $endTime = null; + if (isset($component->DTEND)) { + $endTime = $component->DTEND->getDateTime(); + } elseif (isset($component->DURATION)) { + $duration = DateTimeParser::parseDuration((string)$component->DURATION); + $endTime = clone $startTime; + $endTime->add($duration); + } elseif (!$component->DTSTART->hasTime()) { + $endTime = clone $startTime; + $endTime->modify('+1 day'); + } else { + // The event had no duration (0 seconds) + break; + } + + $times[] = array($startTime, $endTime); + + } + + foreach($times as $time) { + + if ($this->end && $time[0] > $this->end) break; + if ($this->start && $time[1] < $this->start) break; + + $busyTimes[] = array( + $time[0], + $time[1], + $FBTYPE, + ); + } + break; + + case 'VFREEBUSY' : + foreach($component->FREEBUSY as $freebusy) { + + $fbType = isset($freebusy['FBTYPE'])?strtoupper($freebusy['FBTYPE']):'BUSY'; + + // Skipping intervals marked as 'free' + if ($fbType==='FREE') + continue; + + $values = explode(',', $freebusy); + foreach($values as $value) { + list($startTime, $endTime) = explode('/', $value); + $startTime = DateTimeParser::parseDateTime($startTime); + + if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') { + $duration = DateTimeParser::parseDuration($endTime); + $endTime = clone $startTime; + $endTime->add($duration); + } else { + $endTime = DateTimeParser::parseDateTime($endTime); + } + + if($this->start && $this->start > $endTime) continue; + if($this->end && $this->end < $startTime) continue; + $busyTimes[] = array( + $startTime, + $endTime, + $fbType + ); + + } + + + } + break; + + + + } + + + } + + } + + if ($this->baseObject) { + $calendar = $this->baseObject; + } else { + $calendar = new VCalendar(); + } + + $vfreebusy = $calendar->createComponent('VFREEBUSY'); + $calendar->add($vfreebusy); + + if ($this->start) { + $dtstart = $calendar->createProperty('DTSTART'); + $dtstart->setDateTime($this->start); + $vfreebusy->add($dtstart); + } + if ($this->end) { + $dtend = $calendar->createProperty('DTEND'); + $dtend->setDateTime($this->end); + $vfreebusy->add($dtend); + } + $dtstamp = $calendar->createProperty('DTSTAMP'); + $dtstamp->setDateTime(new \DateTime('now', new \DateTimeZone('UTC'))); + $vfreebusy->add($dtstamp); + + foreach($busyTimes as $busyTime) { + + $busyTime[0]->setTimeZone(new \DateTimeZone('UTC')); + $busyTime[1]->setTimeZone(new \DateTimeZone('UTC')); + + $prop = $calendar->createProperty( + 'FREEBUSY', + $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z') + ); + $prop['FBTYPE'] = $busyTime[2]; + $vfreebusy->add($prop); + + } + + return $calendar; + + } + +} + diff --git a/3rdparty/VObject/Node.php b/3rdparty/VObject/Node.php new file mode 100644 index 000000000..583722622 --- /dev/null +++ b/3rdparty/VObject/Node.php @@ -0,0 +1,201 @@ +iterator)) + return $this->iterator; + + return new ElementList(array($this)); + + } + + /** + * Sets the overridden iterator + * + * Note that this is not actually part of the iterator interface + * + * @param ElementList $iterator + * @return void + */ + public function setIterator(ElementList $iterator) { + + $this->iterator = $iterator; + + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * @return array + */ + public function validate($options = 0) { + + return array(); + + } + + /* }}} */ + + /* {{{ Countable interface */ + + /** + * Returns the number of elements + * + * @return int + */ + public function count() { + + $it = $this->getIterator(); + return $it->count(); + + } + + /* }}} */ + + /* {{{ ArrayAccess Interface */ + + + /** + * Checks if an item exists through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @return bool + */ + public function offsetExists($offset) { + + $iterator = $this->getIterator(); + return $iterator->offsetExists($offset); + + } + + /** + * Gets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @return mixed + */ + public function offsetGet($offset) { + + $iterator = $this->getIterator(); + return $iterator->offsetGet($offset); + + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @param mixed $value + * @return void + */ + public function offsetSet($offset,$value) { + + $iterator = $this->getIterator(); + $iterator->offsetSet($offset,$value); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + // @codeCoverageIgnoreEnd + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @return void + */ + public function offsetUnset($offset) { + + $iterator = $this->getIterator(); + $iterator->offsetUnset($offset); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + // @codeCoverageIgnoreEnd + + /* }}} */ +} diff --git a/3rdparty/VObject/Parameter.php b/3rdparty/VObject/Parameter.php new file mode 100644 index 000000000..091013e3b --- /dev/null +++ b/3rdparty/VObject/Parameter.php @@ -0,0 +1,343 @@ +name = strtoupper($name); + $this->root = $root; + if (is_null($name)) { + $this->noName = true; + $this->name = static::guessParameterNameByValue($value); + } + $this->setValue($value); + } + + /** + * Try to guess property name by value, can be used for vCard 2.1 nameless parameters. + * + * Figuring out what the name should have been. Note that a ton of + * these are rather silly in 2013 and would probably rarely be + * used, but we like to be complete. + * + * @param string $value + * @return string + */ + public static function guessParameterNameByValue($value) { + switch(strtoupper($value)) { + + // Encodings + case '7-BIT' : + case 'QUOTED-PRINTABLE' : + case 'BASE64' : + $name = 'ENCODING'; + break; + + // Common types + case 'WORK' : + case 'HOME' : + case 'PREF' : + + // Delivery Label Type + case 'DOM' : + case 'INTL' : + case 'POSTAL' : + case 'PARCEL' : + + // Telephone types + case 'VOICE' : + case 'FAX' : + case 'MSG' : + case 'CELL' : + case 'PAGER' : + case 'BBS' : + case 'MODEM' : + case 'CAR' : + case 'ISDN' : + case 'VIDEO' : + + // EMAIL types (lol) + case 'AOL' : + case 'APPLELINK' : + case 'ATTMAIL' : + case 'CIS' : + case 'EWORLD' : + case 'INTERNET' : + case 'IBMMAIL' : + case 'MCIMAIL' : + case 'POWERSHARE' : + case 'PRODIGY' : + case 'TLX' : + case 'X400' : + + // Photo / Logo format types + case 'GIF' : + case 'CGM' : + case 'WMF' : + case 'BMP' : + case 'DIB' : + case 'PICT' : + case 'TIFF' : + case 'PDF ': + case 'PS' : + case 'JPEG' : + case 'MPEG' : + case 'MPEG2' : + case 'AVI' : + case 'QTIME' : + + // Sound Digital Audio Type + case 'WAVE' : + case 'PCM' : + case 'AIFF' : + + // Key types + case 'X509' : + case 'PGP' : + $name = 'TYPE'; + break; + + // Value types + case 'INLINE' : + case 'URL' : + case 'CONTENT-ID' : + case 'CID' : + $name = 'VALUE'; + break; + + default: + $name = ''; + } + + return $name; + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + * @return void + */ + public function setValue($value) { + + $this->value = $value; + + } + + /** + * Returns the current value + * + * This method will always return a string, or null. If there were multiple + * values, it will automatically concatinate them (separated by comma). + * + * @return string|null + */ + public function getValue() { + + if (is_array($this->value)) { + return implode(',' , $this->value); + } else { + return $this->value; + } + + } + + /** + * Sets multiple values for this parameter. + * + * @param array $value + * @return void + */ + public function setParts(array $value) { + + $this->value = $value; + + } + + /** + * Returns all values for this parameter. + * + * If there were no values, an empty array will be returned. + * + * @return array + */ + public function getParts() { + + if (is_array($this->value)) { + return $this->value; + } elseif (is_null($this->value)) { + return array(); + } else { + return array($this->value); + } + + } + + /** + * Adds a value to this parameter + * + * If the argument is specified as an array, all items will be added to the + * parameter value list. + * + * @param string|array $part + * @return void + */ + public function addValue($part) { + + if (is_null($this->value)) { + $this->value = $part; + } else { + $this->value = array_merge((array)$this->value, (array)$part); + } + + } + + /** + * Checks if this parameter contains the specified value. + * + * This is a case-insensitive match. It makes sense to call this for for + * instance the TYPE parameter, to see if it contains a keyword such as + * 'WORK' or 'FAX'. + * + * @param string $value + * @return bool + */ + public function has($value) { + + return in_array( + strtolower($value), + array_map('strtolower', (array)$this->value) + ); + + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() { + + $value = $this->getParts(); + + if (count($value)===0) { + return $this->name; + } + + if ($this->root->getDocumentType() === Document::VCARD21 && $this->noName) { + + return implode(';', $value); + + } + + return $this->name . '=' . array_reduce($value, function($out, $item) { + + if (!is_null($out)) $out.=','; + + // If there's no special characters in the string, we'll use the simple + // format + if (!preg_match('#(?: [\n":;\^,] )#x', $item)) { + return $out.$item; + } else { + // Enclosing in double-quotes, and using RFC6868 for encoding any + // special characters + $out.='"' . strtr($item, array( + '^' => '^^', + "\n" => '^n', + '"' => '^\'', + )) . '"'; + return $out; + } + + }); + + } + + /** + * This method returns an array, with the representation as it should be + * encoded in json. This is used to create jCard or jCal documents. + * + * @return array + */ + public function jsonSerialize() { + + return $this->value; + + } + + /** + * Called when this object is being cast to a string + * + * @return string + */ + public function __toString() { + + return $this->getValue(); + + } + + /** + * Returns the iterator for this object + * + * @return ElementList + */ + public function getIterator() { + + if (!is_null($this->iterator)) + return $this->iterator; + + return $this->iterator = new ArrayObject((array)$this->value); + + } + +} diff --git a/3rdparty/VObject/ParseException.php b/3rdparty/VObject/ParseException.php new file mode 100644 index 000000000..ba40402c4 --- /dev/null +++ b/3rdparty/VObject/ParseException.php @@ -0,0 +1,12 @@ +setInput($input); + } + if (is_null($this->input)) { + throw new EofException('End of input stream, or no input supplied'); + } + + if (!is_null($options)) { + $this->options = $options; + } + + switch($this->input[0]) { + case 'vcalendar' : + $this->root = new VCalendar(array(), false); + break; + case 'vcard' : + $this->root = new VCard(array(), false); + break; + default : + throw new ParseException('The root component must either be a vcalendar, or a vcard'); + + } + foreach($this->input[1] as $prop) { + $this->root->add($this->parseProperty($prop)); + } + if (isset($this->input[2])) foreach($this->input[2] as $comp) { + $this->root->add($this->parseComponent($comp)); + } + + // Resetting the input so we can throw an feof exception the next time. + $this->input = null; + + return $this->root; + + } + + /** + * Parses a component + * + * @param array $jComp + * @return \Sabre\VObject\Component + */ + public function parseComponent(array $jComp) { + + // We can remove $self from PHP 5.4 onward. + $self = $this; + + $properties = array_map(function($jProp) use ($self) { + return $self->parseProperty($jProp); + }, $jComp[1]); + + if (isset($jComp[2])) { + + $components = array_map(function($jComp) use ($self) { + return $self->parseComponent($jComp); + }, $jComp[2]); + + } else $components = array(); + + return $this->root->createComponent( + $jComp[0], + array_merge( $properties, $components), + $defaults = false + ); + + } + + /** + * Parses properties. + * + * @param array $jProp + * @return \Sabre\VObject\Property + */ + public function parseProperty(array $jProp) { + + list( + $propertyName, + $parameters, + $valueType + ) = $jProp; + + $propertyName = strtoupper($propertyName); + + // This is the default class we would be using if we didn't know the + // value type. We're using this value later in this function. + $defaultPropertyClass = $this->root->getClassNameForPropertyName($propertyName); + + $parameters = (array)$parameters; + + $value = array_slice($jProp, 3); + + $valueType = strtoupper($valueType); + + if (isset($parameters['group'])) { + $propertyName = $parameters['group'] . '.' . $propertyName; + unset($parameters['group']); + } + + $prop = $this->root->createProperty($propertyName, null, $parameters, $valueType); + $prop->setJsonValue($value); + + // We have to do something awkward here. FlatText as well as Text + // represents TEXT values. We have to normalize these here. In the + // future we can get rid of FlatText once we're allowed to break BC + // again. + if ($defaultPropertyClass === 'Sabre\VObject\Property\FlatText') { + $defaultPropertyClass = 'Sabre\VObject\Property\Text'; + } + + // If the value type we received (e.g.: TEXT) was not the default value + // type for the given property (e.g.: BDAY), we need to add a VALUE= + // parameter. + if ($defaultPropertyClass !== get_class($prop)) { + $prop["VALUE"] = $valueType; + } + + return $prop; + + } + + /** + * Sets the input data + * + * @param resource|string|array $input + * @return void + */ + public function setInput($input) { + + if (is_resource($input)) { + $input = stream_get_contents($input); + } + if (is_string($input)) { + $input = json_decode($input); + } + $this->input = $input; + + } + +} diff --git a/3rdparty/VObject/Parser/MimeDir.php b/3rdparty/VObject/Parser/MimeDir.php new file mode 100644 index 000000000..bc48e07bb --- /dev/null +++ b/3rdparty/VObject/Parser/MimeDir.php @@ -0,0 +1,602 @@ +root = null; + if (!is_null($input)) { + + $this->setInput($input); + + } + + if (!is_null($options)) $this->options = $options; + + $this->parseDocument(); + + return $this->root; + + } + + /** + * Sets the input buffer. Must be a string or stream. + * + * @param resource|string $input + * @return void + */ + public function setInput($input) { + + // Resetting the parser + $this->lineIndex = 0; + $this->startLine = 0; + + if (is_string($input)) { + // Convering to a stream. + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $input); + rewind($stream); + $this->input = $stream; + } else { + $this->input = $input; + } + + } + + /** + * Parses an entire document. + * + * @return void + */ + protected function parseDocument() { + + $line = $this->readLine(); + switch(strtoupper($line)) { + case 'BEGIN:VCALENDAR' : + $class = isset(VCalendar::$componentMap['VCALENDAR']) + ? VCalendar::$componentMap[$name] + : '\\OCA\\Calendar\\Sabre\\VObject\\Component\\VCalendar'; + break; + case 'BEGIN:VCARD' : + $class = isset(VCard::$componentMap['VCARD']) + ? VCard::$componentMap['VCARD'] + : '\\OCA\\Calendar\\Sabre\\VObject\\Component\\VCard'; + break; + default : + throw new ParseException('This parser only supports VCARD and VCALENDAR files'); + } + + $this->root = new $class(array(), false); + + while(true) { + + // Reading until we hit END: + $line = $this->readLine(); + if (strtoupper(substr($line,0,4)) === 'END:') { + break; + } + $result = $this->parseLine($line); + if ($result) { + $this->root->add($result); + } + + } + + $name = strtoupper(substr($line, 4)); + if ($name!==$this->root->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:' . $this->root->name . '" got: "END:' . $name . '"'); + } + + } + + /** + * Parses a line, and if it hits a component, it will also attempt to parse + * the entire component + * + * @param string $line Unfolded line + * @return Node + */ + protected function parseLine($line) { + + // Start of a new component + if (strtoupper(substr($line, 0, 6)) === 'BEGIN:') { + + $component = $this->root->createComponent(substr($line,6), array(), false); + + while(true) { + + // Reading until we hit END: + $line = $this->readLine(); + if (strtoupper(substr($line,0,4)) === 'END:') { + break; + } + $result = $this->parseLine($line); + if ($result) { + $component->add($result); + } + + } + + $name = strtoupper(substr($line, 4)); + if ($name!==$component->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:' . $component->name . '" got: "END:' . $name . '"'); + } + + return $component; + + } else { + + // Property reader + $property = $this->readProperty($line); + if (!$property) { + // Ignored line + return false; + } + return $property; + + } + + } + + /** + * We need to look ahead 1 line every time to see if we need to 'unfold' + * the next line. + * + * If that was not the case, we store it here. + * + * @var null|string + */ + protected $lineBuffer; + + /** + * The real current line number. + */ + protected $lineIndex = 0; + + /** + * In the case of unfolded lines, this property holds the line number for + * the start of the line. + * + * @var int + */ + protected $startLine = 0; + + /** + * Contains a 'raw' representation of the current line. + * + * @var string + */ + protected $rawLine; + + /** + * Reads a single line from the buffer. + * + * This method strips any newlines and also takes care of unfolding. + * + * @throws \Sabre\VObject\EofException + * @return string + */ + protected function readLine() { + + if (!is_null($this->lineBuffer)) { + $rawLine = $this->lineBuffer; + $this->lineBuffer = null; + } else { + do { + $rawLine = fgets($this->input); + if ($rawLine === false && feof($this->input)) { + throw new EofException('End of document reached prematurely'); + } + $rawLine = rtrim($rawLine, "\r\n"); + } while ($rawLine === ''); // Skipping empty lines + $this->lineIndex++; + } + $line = $rawLine; + + $this->startLine = $this->lineIndex; + + // Looking ahead for folded lines. + while (true) { + + $nextLine = rtrim(fgets($this->input), "\r\n"); + $this->lineIndex++; + if (!$nextLine) { + break; + } + if ($nextLine[0] === "\t" || $nextLine[0] === " ") { + $line .= substr($nextLine, 1); + $rawLine .= "\n " . substr($nextLine, 1); + } else { + $this->lineBuffer = $nextLine; + break; + } + + } + $this->rawLine = $rawLine; + return $line; + + } + + /** + * Reads a property or component from a line. + * + * @return void + */ + protected function readProperty($line) { + + if ($this->options & self::OPTION_FORGIVING) { + $propNameToken = 'A-Z0-9\-\._\\/'; + } else { + $propNameToken = 'A-Z0-9\-\.'; + } + + $paramNameToken = 'A-Z0-9\-'; + $safeChar = '^";:,'; + $qSafeChar = '^"'; + + $regex = "/ + ^(?P [$propNameToken]+ ) (?=[;:]) # property name + | + (?<=:)(?P .*)$ # property value + | + ;(?P [$paramNameToken]+) (?=[=;:]) # parameter name + | + (=|,)(?P # parameter value + (?: [$safeChar]*) | + \"(?: [$qSafeChar]+)\" + ) (?=[;:,]) + /xi"; + + //echo $regex, "\n"; die(); + preg_match_all($regex, $line, $matches, PREG_SET_ORDER ); + + $property = array( + 'name' => null, + 'parameters' => array(), + 'value' => null + ); + + $lastParam = null; + + /** + * Looping through all the tokens. + * + * Note that we are looping through them in reverse order, because if a + * sub-pattern matched, the subsequent named patterns will not show up + * in the result. + */ + foreach($matches as $match) { + + if (isset($match['paramValue'])) { + if ($match['paramValue'] && $match['paramValue'][0] === '"') { + $value = substr($match['paramValue'], 1, -1); + } else { + $value = $match['paramValue']; + } + + $value = $this->unescapeParam($value); + + if (is_null($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam] = $value; + } elseif (is_array($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam][] = $value; + } else { + $property['parameters'][$lastParam] = array( + $property['parameters'][$lastParam], + $value + ); + } + continue; + } + if (isset($match['paramName'])) { + $lastParam = strtoupper($match['paramName']); + if (!isset($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam] = null; + } + continue; + } + if (isset($match['propValue'])) { + $property['value'] = $match['propValue']; + continue; + } + if (isset($match['name']) && $match['name']) { + $property['name'] = strtoupper($match['name']); + continue; + } + + // @codeCoverageIgnoreStart + throw new \LogicException('This code should not be reachable'); + // @codeCoverageIgnoreEnd + + } + + if (is_null($property['value']) || !$property['name']) { + if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { + return false; + } + throw new ParseException('Invalid Mimedir file. Line starting at ' . $this->startLine . ' did not follow iCalendar/vCard conventions'); + } + + // vCard 2.1 states that parameters may appear without a name, and only + // a value. We can deduce the value based on it's name. + // + // Our parser will get those as parameters without a value instead, so + // we're filtering these parameters out first. + $namedParameters = array(); + $namelessParameters = array(); + + foreach($property['parameters'] as $name=>$value) { + if (!is_null($value)) { + $namedParameters[$name] = $value; + } else { + $namelessParameters[] = $name; + } + } + + $propObj = $this->root->createProperty($property['name'], null, $namedParameters); + + foreach($namelessParameters as $namelessParameter) { + $propObj->add(null, $namelessParameter); + } + + if (strtoupper($propObj['ENCODING']) === 'QUOTED-PRINTABLE') { + $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue()); + } else { + $propObj->setRawMimeDirValue($property['value']); + } + + return $propObj; + + } + + /** + * Unescapes a property value. + * + * vCard 2.1 says: + * * Semi-colons must be escaped in some property values, specifically + * ADR, ORG and N. + * * Semi-colons must be escaped in parameter values, because semi-colons + * are also use to separate values. + * * No mention of escaping backslashes with another backslash. + * * newlines are not escaped either, instead QUOTED-PRINTABLE is used to + * span values over more than 1 line. + * + * vCard 3.0 says: + * * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be + * escaped, all time time. + * * Comma's are used for delimeters in multiple values + * * (rfc2426) Adds to to this that the semi-colon MUST also be escaped, + * as in some properties semi-colon is used for separators. + * * Properties using semi-colons: N, ADR, GEO, ORG + * * Both ADR and N's individual parts may be broken up further with a + * comma. + * * Properties using commas: NICKNAME, CATEGORIES + * + * vCard 4.0 (rfc6350) says: + * * Commas must be escaped. + * * Semi-colons may be escaped, an unescaped semi-colon _may_ be a + * delimiter, depending on the property. + * * Backslashes must be escaped + * * Newlines must be escaped as either \N or \n. + * * Some compound properties may contain multiple parts themselves, so a + * comma within a semi-colon delimited property may also be unescaped + * to denote multiple parts _within_ the compound property. + * * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP. + * * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID. + * + * Even though the spec says that commas must always be escaped, the + * example for GEO in Section 6.5.2 seems to violate this. + * + * iCalendar 2.0 (rfc5545) says: + * * Commas or semi-colons may be used as delimiters, depending on the + * property. + * * Commas, semi-colons, backslashes, newline (\N or \n) are always + * escaped, unless they are delimiters. + * * Colons shall not be escaped. + * * Commas can be considered the 'default delimiter' and is described as + * the delimiter in cases where the order of the multiple values is + * insignificant. + * * Semi-colons are described as the delimiter for 'structured values'. + * They are specifically used in Semi-colons are used as a delimiter in + * REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however. + * + * Now for the parameters + * + * If delimiter is not set (null) this method will just return a string. + * If it's a comma or a semi-colon the string will be split on those + * characters, and always return an array. + * + * @param string $input + * @param string $delimiter + * @return string|string[] + */ + static public function unescapeValue($input, $delimiter = ';') { + + $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )'; + if ($delimiter) { + $regex .= ' | (' . $delimiter . ')'; + } + $regex .= ') #x'; + + $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); + + $resultArray = array(); + $result = ''; + + foreach($matches as $match) { + + switch ($match) { + case '\\\\' : + $result .='\\'; + break; + case '\N' : + case '\n' : + $result .="\n"; + break; + case '\;' : + $result .=';'; + break; + case '\,' : + $result .=','; + break; + case $delimiter : + $resultArray[] = $result; + $result = ''; + break; + default : + $result .= $match; + break; + + } + + } + + $resultArray[] = $result; + return $delimiter ? $resultArray : $result; + + } + + /** + * Unescapes a parameter value. + * + * vCard 2.1: + * * Does not mention a mechanism for this. In addition, double quotes + * are never used to wrap values. + * * This means that parameters can simply not contain colons or + * semi-colons. + * + * vCard 3.0 (rfc2425, rfc2426): + * * Parameters _may_ be surrounded by double quotes. + * * If this is not the case, semi-colon, colon and comma may simply not + * occur (the comma used for multiple parameter values though). + * * If it is surrounded by double-quotes, it may simply not contain + * double-quotes. + * * This means that a parameter can in no case encode double-quotes, or + * newlines. + * + * vCard 4.0 (rfc6350) + * * Behavior seems to be identical to vCard 3.0 + * + * iCalendar 2.0 (rfc5545) + * * Behavior seems to be identical to vCard 3.0 + * + * Parameter escaping mechanism (rfc6868) : + * * This rfc describes a new way to escape parameter values. + * * New-line is encoded as ^n + * * ^ is encoded as ^^. + * * " is encoded as ^' + * + * @param string $input + * @return void + */ + private function unescapeParam($input) { + + return + preg_replace_callback('#(\^(\^|n|\'))#',function($matches) { + switch($matches[2]) { + case 'n' : + return "\n"; + case '^' : + return '^'; + case '\'' : + return '"'; + + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + }, $input); + + } + + /** + * Gets the full quoted printable value. + * + * We need a special method for this, because newlines have both a meaning + * in vCards, and in QuotedPrintable. + * + * This method does not do any decoding. + * + * @return string + */ + private function extractQuotedPrintableValue() { + + // We need to parse the raw line again to get the start of the value. + // + // We are basically looking for the first colon (:), but we need to + // skip over the parameters first, as they may contain one. + $regex = '/^ + (?: [^:])+ # Anything but a colon + (?: "[^"]")* # A parameter in double quotes + : # start of the value we really care about + (.*)$ + /xs'; + + preg_match($regex, $this->rawLine, $matches); + + $value = $matches[1]; + // Removing the first whitespace character from every line. Kind of + // like unfolding, but we keep the newline. + $value = str_replace("\n ", "\n", $value); + + // Microsoft products don't always correctly fold lines, they may be + // missing a whitespace. So if 'forgiving' is turned on, we will take + // those as well. + if ($this->options & self::OPTION_FORGIVING) { + while(substr($value,-1) === '=') { + // Reading the line + $this->readLine(); + // Grabbing the raw form + $value.="\n" . $this->rawLine; + } + } + + return $value; + + } + +} diff --git a/3rdparty/VObject/Parser/Parser.php b/3rdparty/VObject/Parser/Parser.php new file mode 100644 index 000000000..1c66b8ebe --- /dev/null +++ b/3rdparty/VObject/Parser/Parser.php @@ -0,0 +1,77 @@ +setInput($input); + } + $this->options = $options; + } + + /** + * This method starts the parsing process. + * + * If the input was not supplied during construction, it's possible to pass + * it here instead. + * + * If either input or options are not supplied, the defaults will be used. + * + * @param mixed $input + * @param int|null $options + * @return array + */ + abstract public function parse($input = null, $options = null); + + /** + * Sets the input data + * + * @param mixed $input + * @return void + */ + abstract public function setInput($input); + +} diff --git a/3rdparty/VObject/Property.php b/3rdparty/VObject/Property.php new file mode 100644 index 000000000..53abd4940 --- /dev/null +++ b/3rdparty/VObject/Property.php @@ -0,0 +1,502 @@ +value syntax. + * + * @param Component $root The root document + * @param string $name + * @param string|array|null $value + * @param array $parameters List of parameters + * @param string $group The vcard property group + * @return void + */ + public function __construct(Component $root, $name, $value = null, array $parameters = array(), $group = null) { + + $this->name = $name; + $this->group = $group; + + $this->root = $root; + + if (!is_null($value)) { + $this->setValue($value); + } + + foreach($parameters as $k=>$v) { + $this->add($k, $v); + } + + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + * @return void + */ + public function setValue($value) { + + $this->value = $value; + + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + * + * @return string + */ + public function getValue() { + + if (is_array($this->value)) { + if (count($this->value)==0) { + return null; + } elseif (count($this->value)===1) { + return $this->value[0]; + } else { + return $this->getRawMimeDirValue($this->value); + } + } else { + return $this->value; + } + + } + + /** + * Sets a multi-valued property. + * + * @param array $parts + * @return void + */ + public function setParts(array $parts) { + + $this->value = $parts; + + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + * + * @return array + */ + public function getParts() { + + if (is_null($this->value)) { + return array(); + } elseif (is_array($this->value)) { + return $this->value; + } else { + return array($this->value); + } + + } + + /** + * Adds a new parameter, and returns the new item. + * + * If a parameter with same name already existed, the values will be + * combined. + * If nameless parameter is added, we try to guess it's name. + * + * @param string $name + * @param string|null|array $value + * @return Node + */ + public function add($name, $value = null) { + $noName = false; + if ($name === null) { + $name = Parameter::guessParameterNameByValue($value); + $noName = true; + } + + if (isset($this->parameters[strtoupper($name)])) { + $this->parameters[strtoupper($name)]->addValue($value); + } + else { + $param = new Parameter($this->root, $name, $value); + $param->noName = $noName; + $this->parameters[$param->name] = $param; + } + } + + /** + * Returns an iterable list of children + * + * @return array + */ + public function parameters() { + + return $this->parameters; + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + abstract public function getValueType(); + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + * @return void + */ + abstract public function setRawMimeDirValue($val); + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + abstract public function getRawMimeDirValue(); + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() { + + $str = $this->name; + if ($this->group) $str = $this->group . '.' . $this->name; + + foreach($this->parameters as $param) { + + $str.=';' . $param->serialize(); + + } + + $str.=':' . $this->getRawMimeDirValue(); + + $out = ''; + while(strlen($str)>0) { + if (strlen($str)>75) { + $out.= mb_strcut($str,0,75,'utf-8') . "\r\n"; + $str = ' ' . mb_strcut($str,75,strlen($str),'utf-8'); + } else { + $out.=$str . "\r\n"; + $str=''; + break; + } + } + + return $out; + + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() { + + return $this->getParts(); + + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + * + * @param array $value + * @return void + */ + public function setJsonValue(array $value) { + + if (count($value)===1) { + $this->setValue(reset($value)); + } else { + $this->setValue($value); + } + + } + + /** + * This method returns an array, with the representation as it should be + * encoded in json. This is used to create jCard or jCal documents. + * + * @return array + */ + public function jsonSerialize() { + + $parameters = array(); + + foreach($this->parameters as $parameter) { + if ($parameter->name === 'VALUE') { + continue; + } + $parameters[strtolower($parameter->name)] = $parameter->jsonSerialize(); + } + // In jCard, we need to encode the property-group as a separate 'group' + // parameter. + if ($this->group) { + $parameters['group'] = $this->group; + } + + return array_merge( + array( + strtolower($this->name), + (object)$parameters, + strtolower($this->getValueType()), + ), + $this->getJsonValue() + ); + } + + + /** + * Called when this object is being cast to a string. + * + * If the property only had a single value, you will get just that. In the + * case the property had multiple values, the contents will be escaped and + * combined with ,. + * + * @return string + */ + public function __toString() { + + return (string)$this->getValue(); + + } + + /* ArrayAccess interface {{{ */ + + /** + * Checks if an array element exists + * + * @param mixed $name + * @return bool + */ + public function offsetExists($name) { + + if (is_int($name)) return parent::offsetExists($name); + + $name = strtoupper($name); + + foreach($this->parameters as $parameter) { + if ($parameter->name == $name) return true; + } + return false; + + } + + /** + * Returns a parameter. + * + * If the parameter does not exist, null is returned. + * + * @param string $name + * @return Node + */ + public function offsetGet($name) { + + if (is_int($name)) return parent::offsetGet($name); + $name = strtoupper($name); + + if (!isset($this->parameters[$name])) { + return null; + } + + return $this->parameters[$name]; + + } + + /** + * Creates a new parameter + * + * @param string $name + * @param mixed $value + * @return void + */ + public function offsetSet($name, $value) { + + if (is_int($name)) { + parent::offsetSet($name, $value); + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + $param = new Parameter($this->root, $name, $value); + $this->parameters[$param->name] = $param; + + } + + /** + * Removes one or more parameters with the specified name + * + * @param string $name + * @return void + */ + public function offsetUnset($name) { + + if (is_int($name)) { + parent::offsetUnset($name); + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + unset($this->parameters[strtoupper($name)]); + + } + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + * + * @return void + */ + public function __clone() { + + foreach($this->parameters as $key=>$child) { + $this->parameters[$key] = clone $child; + $this->parameters[$key]->parent = $this; + } + + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * @return array + */ + public function validate($options = 0) { + + $warnings = array(); + + // Checking if our value is UTF-8 + if (!StringUtil::isUTF8($this->getRawMimeDirValue())) { + $warnings[] = array( + 'level' => 1, + 'message' => 'Property is not valid UTF-8!', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->setRawMimeDirValue(StringUtil::convertToUTF8($this->getRawMimeDirValue())); + } + } + + // Checking if the propertyname does not contain any invalid bytes. + if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The propertyname: ' . $this->name . ' contains invalid characters. Only A-Z, 0-9 and - are allowed', + 'node' => $this, + ); + if ($options & self::REPAIR) { + // Uppercasing and converting underscores to dashes. + $this->name = strtoupper( + str_replace('_', '-', $this->name) + ); + // Removing every other invalid character + $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name); + + } + + } + + // Validating inner parameters + foreach($this->parameters as $param) { + $warnings = array_merge($warnings, $param->validate($options)); + } + + return $warnings; + + } + +} diff --git a/3rdparty/VObject/Property/Binary.php b/3rdparty/VObject/Property/Binary.php new file mode 100644 index 000000000..c9a2bf5a5 --- /dev/null +++ b/3rdparty/VObject/Property/Binary.php @@ -0,0 +1,127 @@ +value = $value[0]; + } else { + throw new \InvalidArgumentException('The argument must either be a string or an array with only one child'); + } + + } else { + + $this->value = $value; + + } + + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + * @return void + */ + public function setRawMimeDirValue($val) { + + $this->value = base64_decode($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + return base64_encode($this->value); + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return 'BINARY'; + + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() { + + return array(base64_encode($this->getValue())); + + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + * + * @param array $value + * @return void + */ + public function setJsonValue(array $value) { + + $value = array_map('base64_decode', $value); + parent::setJsonValue($value); + + } + +} diff --git a/3rdparty/VObject/Property/Boolean.php b/3rdparty/VObject/Property/Boolean.php new file mode 100644 index 000000000..84cc8d586 --- /dev/null +++ b/3rdparty/VObject/Property/Boolean.php @@ -0,0 +1,63 @@ +setValue($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + return $this->value?'TRUE':'FALSE'; + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return 'BOOLEAN'; + + } + +} diff --git a/3rdparty/VObject/Property/FlatText.php b/3rdparty/VObject/Property/FlatText.php new file mode 100644 index 000000000..a4c7c3a76 --- /dev/null +++ b/3rdparty/VObject/Property/FlatText.php @@ -0,0 +1,49 @@ +setValue($val); + + } + +} diff --git a/3rdparty/VObject/Property/Float.php b/3rdparty/VObject/Property/Float.php new file mode 100644 index 000000000..f470eb80b --- /dev/null +++ b/3rdparty/VObject/Property/Float.php @@ -0,0 +1,101 @@ +delimiter, $val); + foreach($val as &$item) { + $item = (float)$item; + } + $this->setParts($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + return implode( + $this->delimiter, + $this->getParts() + ); + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return "FLOAT"; + + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() { + + $val = array_map(function($item) { + + return (float)$item; + + }, $this->getParts()); + + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-04#section-3.4.1.2 + if ($this->name==='GEO') { + return array($val); + } else { + return $val; + } + + } +} diff --git a/3rdparty/VObject/Property/ICalendar/CalAddress.php b/3rdparty/VObject/Property/ICalendar/CalAddress.php new file mode 100644 index 000000000..df4f0c5d2 --- /dev/null +++ b/3rdparty/VObject/Property/ICalendar/CalAddress.php @@ -0,0 +1,41 @@ +setDateTimes($parts); + } else { + parent::setParts($parts); + } + + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * Instead of strings, you may also use DateTime here. + * + * @param string|array|\DateTime $value + * @return void + */ + public function setValue($value) { + + if (is_array($value) && isset($value[0]) && $value[0] instanceof \DateTime) { + $this->setDateTimes($value); + } elseif ($value instanceof \DateTime) { + $this->setDateTimes(array($value)); + } else { + parent::setValue($value); + } + + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + * @return void + */ + public function setRawMimeDirValue($val) { + + $this->setValue(explode($this->delimiter, $val)); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + return implode($this->delimiter, $this->getParts()); + + } + + /** + * Returns true if this is a DATE-TIME value, false if it's a DATE. + * + * @return bool + */ + public function hasTime() { + + return strtoupper((string)$this['VALUE']) !== 'DATE'; + + } + + /** + * Returns a date-time value. + * + * Note that if this property contained more than 1 date-time, only the + * first will be returned. To get an array with multiple values, call + * getDateTimes. + * + * @return \DateTime + */ + public function getDateTime() { + + $dt = $this->getDateTimes(); + if (!$dt) return null; + + return $dt[0]; + + } + + /** + * Returns multiple date-time values. + * + * @return \DateTime[] + */ + public function getDateTimes() { + + // Finding the timezone. + $tz = $this['TZID']; + + if ($tz) { + $tz = TimeZoneUtil::getTimeZone((string)$tz, $this->root); + } + + $dts = array(); + foreach($this->getParts() as $part) { + $dts[] = DateTimeParser::parse($part, $tz); + } + return $dts; + + } + + /** + * Sets the property as a DateTime object. + * + * @param \DateTime $dt + * @param bool isFloating If set to true, timezones will be ignored. + * @return void + */ + public function setDateTime(\DateTime $dt, $isFloating = false) { + + $this->setDateTimes(array($dt), $isFloating); + + } + + /** + * Sets the property as multiple date-time objects. + * + * The first value will be used as a reference for the timezones, and all + * the otehr values will be adjusted for that timezone + * + * @param \DateTime[] $dt + * @param bool isFloating If set to true, timezones will be ignored. + * @return void + */ + public function setDateTimes(array $dt, $isFloating = false) { + + $values = array(); + + if($this->hasTime()) { + + $tz = null; + $isUtc = false; + + foreach($dt as $d) { + + if ($isFloating) { + $values[] = $d->format('Ymd\\THis'); + continue; + } + if (is_null($tz)) { + $tz = $d->getTimeZone(); + $isUtc = in_array($tz->getName() , array('UTC', 'GMT', 'Z')); + if (!$isUtc) { + $this->offsetSet('TZID', $tz->getName()); + } + } else { + $d->setTimeZone($tz); + } + + if ($isUtc) { + $values[] = $d->format('Ymd\\THis\\Z'); + } else { + $values[] = $d->format('Ymd\\THis'); + } + + } + if ($isUtc || $isFloating) { + $this->offsetUnset('TZID'); + } + + } else { + + foreach($dt as $d) { + + $values[] = $d->format('Ymd'); + + } + $this->offsetUnset('TZID'); + + } + + $this->value = $values; + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return $this->hasTime()?'DATE-TIME':'DATE'; + + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() { + + $dts = $this->getDateTimes(); + $hasTime = $this->hasTime(); + + $tz = $dts[0]->getTimeZone(); + $isUtc = in_array($tz->getName() , array('UTC', 'GMT', 'Z')); + + return array_map(function($dt) use ($hasTime, $isUtc) { + + if ($hasTime) { + return $dt->format('Y-m-d\\TH:i:s') . ($isUtc?'Z':''); + } else { + return $dt->format('Y-m-d'); + } + + }, $dts); + + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + * + * @param array $value + * @return void + */ + public function setJsonValue(array $value) { + + // dates and times in jCal have one difference to dates and times in + // iCalendar. In jCal date-parts are separated by dashes, and + // time-parts are separated by colons. It makes sense to just remove + // those. + $this->setValue(array_map(function($item) { + + return strtr($item, array(':'=>'', '-'=>'')); + + }, $value)); + + } + /** + * We need to intercept offsetSet, because it may be used to alter the + * VALUE from DATE-TIME to DATE or vice-versa. + * + * @param string $name + * @param mixed $value + * @return void + */ + public function offsetSet($name, $value) { + + parent::offsetSet($name, $value); + if (strtoupper($name)!=='VALUE') { + return; + } + + // This will ensure that dates are correctly encoded. + $this->setDateTimes($this->getDateTimes()); + + } +} diff --git a/3rdparty/VObject/Property/ICalendar/Duration.php b/3rdparty/VObject/Property/ICalendar/Duration.php new file mode 100644 index 000000000..b8bcbc54a --- /dev/null +++ b/3rdparty/VObject/Property/ICalendar/Duration.php @@ -0,0 +1,86 @@ +setValue(explode($this->delimiter, $val)); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + return implode($this->delimiter, $this->getParts()); + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return 'DURATION'; + + } + + /** + * Returns a DateInterval representation of the Duration property. + * + * If the property has more than one value, only the first is returned. + * + * @return \DateInterval + */ + public function getDateInterval() { + + $parts = $this->getParts(); + $value = $parts[0]; + return DateTimeParser::parseDuration($value); + + } + +} diff --git a/3rdparty/VObject/Property/ICalendar/Period.php b/3rdparty/VObject/Property/ICalendar/Period.php new file mode 100644 index 000000000..829dd2b90 --- /dev/null +++ b/3rdparty/VObject/Property/ICalendar/Period.php @@ -0,0 +1,126 @@ +setValue(explode($this->delimiter, $val)); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + return implode($this->delimiter, $this->getParts()); + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return "PERIOD"; + + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + * + * @param array $value + * @return void + */ + public function setJsonValue(array $value) { + + $value = array_map(function($item) { + + return strtr(implode('/', $item), array(':' => '', '-' => '')); + + }, $value); + parent::setJsonValue($value); + + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() { + + $return = array(); + foreach($this->getParts() as $item) { + + list($start, $end) = explode('/', $item, 2); + + $start = DateTimeParser::parseDateTime($start); + + // This is a duration value. + if ($end[0]==='P') { + $return[] = array( + $start->format('Y-m-d\\TH:i:s'), + $end + ); + } else { + $end = DateTimeParser::parseDateTime($end); + $return[] = array( + $start->format('Y-m-d\\TH:i:s'), + $end->format('Y-m-d\\TH:i:s'), + ); + } + + } + + return $return; + + } + +} diff --git a/3rdparty/VObject/Property/ICalendar/Recur.php b/3rdparty/VObject/Property/ICalendar/Recur.php new file mode 100644 index 000000000..9afa078ec --- /dev/null +++ b/3rdparty/VObject/Property/ICalendar/Recur.php @@ -0,0 +1,189 @@ +value array that is accessible using + * getParts, and may be set using setParts. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class Recur extends Property { + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + * @return void + */ + public function setValue($value) { + + // If we're getting the data from json, we'll be receiving an object + if ($value instanceof \StdClass) { + $value = (array)$value; + } + + if (is_array($value)) { + $newVal = array(); + foreach($value as $k=>$v) { + + if (is_string($v)) { + $v = strtoupper($v); + + // The value had multiple sub-values + if (strpos($v,',')!==false) { + $v = explode(',', $v); + } + } else { + $v = array_map('strtoupper', $v); + } + + $newVal[strtoupper($k)] = $v; + } + $this->value = $newVal; + } elseif (is_string($value)) { + $value = strtoupper($value); + $newValue = array(); + foreach(explode(';', $value) as $part) { + + // Skipping empty parts. + if (empty($part)) { + continue; + } + list($partName, $partValue) = explode('=', $part); + + // The value itself had multiple values.. + if (strpos($partValue,',')!==false) { + $partValue=explode(',', $partValue); + } + $newValue[$partName] = $partValue; + + } + $this->value = $newValue; + } else { + throw new \InvalidArgumentException('You must either pass a string, or a key=>value array'); + } + + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + * + * @return string + */ + public function getValue() { + + $out = array(); + foreach($this->value as $key=>$value) { + $out[] = $key . '=' . (is_array($value)?implode(',', $value):$value); + } + return strtoupper(implode(';',$out)); + + } + + /** + * Sets a multi-valued property. + * + * @param array $parts + * @return void + */ + public function setParts(array $parts) { + + $this->setValue($parts); + + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + * + * @return array + */ + public function getParts() { + + return $this->value; + + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + * @return void + */ + public function setRawMimeDirValue($val) { + + $this->setValue($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + return $this->getValue(); + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return "RECUR"; + + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() { + + $values = array(); + foreach($this->getParts() as $k=>$v) { + $values[strtolower($k)] = $v; + } + return array($values); + + } +} diff --git a/3rdparty/VObject/Property/Integer.php b/3rdparty/VObject/Property/Integer.php new file mode 100644 index 000000000..2135caeef --- /dev/null +++ b/3rdparty/VObject/Property/Integer.php @@ -0,0 +1,72 @@ +setValue((int)$val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + return $this->value; + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return "INTEGER"; + + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() { + + return array((int)$this->getValue()); + + } +} diff --git a/3rdparty/VObject/Property/Text.php b/3rdparty/VObject/Property/Text.php new file mode 100644 index 000000000..ba5440a0c --- /dev/null +++ b/3rdparty/VObject/Property/Text.php @@ -0,0 +1,330 @@ + 5, + 'ADR' => 7, + ); + + /** + * Creates the property. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param Component $root The root document + * @param string $name + * @param string|array|null $value + * @param array $parameters List of parameters + * @param string $group The vcard property group + * @return void + */ + public function __construct(Component $root, $name, $value = null, array $parameters = array(), $group = null) { + + // There's two types of multi-valued text properties: + // 1. multivalue properties. + // 2. structured value properties + // + // The former is always separated by a comma, the latter by semi-colon. + if (in_array($name, $this->structuredValues)) { + $this->delimiter = ';'; + } + + parent::__construct($root, $name, $value, $parameters, $group); + + } + + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + * @return void + */ + public function setRawMimeDirValue($val) { + + $this->setValue(MimeDir::unescapeValue($val, $this->delimiter)); + + } + + /** + * Sets the value as a quoted-printable encoded string. + * + * @param string $val + * @return void + */ + public function setQuotedPrintableValue($val) { + + $val = quoted_printable_decode($val); + + // Quoted printable only appears in vCard 2.1, and the only character + // that may be escaped there is ;. So we are simply splitting on just + // that. + // + // We also don't have to unescape \\, so all we need to look for is a ; + // that's not preceeded with a \. + $regex = '# (?setValue($matches); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + foreach($val as &$item) { + + if (!is_array($item)) { + $item = array($item); + } + + foreach($item as &$subItem) { + $subItem = strtr($subItem, array( + '\\' => '\\\\', + ';' => '\;', + ',' => '\,', + "\n" => '\n', + "\r" => "", + )); + } + $item = implode(',', $item); + + } + + return implode($this->delimiter, $val); + + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() { + + // Structured text values should always be returned as a single + // array-item. Multi-value text should be returned as multiple items in + // the top-array. + if (in_array($this->name, $this->structuredValues)) { + return array($this->getParts()); + } else { + return $this->getParts(); + } + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return "TEXT"; + + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() { + + // We need to kick in a special type of encoding, if it's a 2.1 vcard. + if ($this->root->getDocumentType() !== Document::VCARD21) { + return parent::serialize(); + } + + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + // Imploding multiple parts into a single value, and splitting the + // values with ;. + if (count($val)>1) { + foreach($val as $k=>$v) { + $val[$k] = str_replace(';','\;', $v); + } + $val = implode(';', $val); + } else { + $val = $val[0]; + } + + $str = $this->name; + if ($this->group) $str = $this->group . '.' . $this->name; + foreach($this->parameters as $param) { + + if ($param->getValue() === 'QUOTED-PRINTABLE') { + continue; + } + $str.=';' . $param->serialize(); + + } + + + + // If the resulting value contains a \n, we must encode it as + // quoted-printable. + if (strpos($val,"\n") !== false) { + + $str.=';ENCODING=QUOTED-PRINTABLE:'; + $lastLine=$str; + $out = null; + + // The PHP built-in quoted-printable-encode does not correctly + // encode newlines for us. Specifically, the \r\n sequence must in + // vcards be encoded as =0D=OA and we must insert soft-newlines + // every 75 bytes. + for($ii=0;$ii= 32 && $ord <=126) { + $lastLine.=$val[$ii]; + } else { + $lastLine.='=' . strtoupper(bin2hex($val[$ii])); + } + if (strlen($lastLine)>=75) { + // Soft line break + $out.=$lastLine. "=\r\n "; + $lastLine = null; + } + + } + if (!is_null($lastLine)) $out.= $lastLine . "\r\n"; + return $out; + + } else { + $str.=':' . $val; + $out = ''; + while(strlen($str)>0) { + if (strlen($str)>75) { + $out.= mb_strcut($str,0,75,'utf-8') . "\r\n"; + $str = ' ' . mb_strcut($str,75,strlen($str),'utf-8'); + } else { + $out.=$str . "\r\n"; + $str=''; + break; + } + } + + return $out; + + + } + + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * @return array + */ + public function validate($options = 0) { + + $warnings = parent::validate($options); + + if (isset($this->minimumPropertyValues[$this->name])) { + + $minimum = $this->minimumPropertyValues[$this->name]; + $parts = $this->getParts(); + if (count($parts) < $minimum) { + $warnings[] = array( + 'level' => 1, + 'message' => 'This property must have at least ' . $minimum . ' components. It only has ' . count($parts), + 'node' => $this, + ); + if ($options & self::REPAIR) { + $parts = array_pad($parts, $minimum, ''); + $this->setParts($parts); + } + } + + } + return $warnings; + + } +} diff --git a/3rdparty/VObject/Property/Time.php b/3rdparty/VObject/Property/Time.php new file mode 100644 index 000000000..cb5196882 --- /dev/null +++ b/3rdparty/VObject/Property/Time.php @@ -0,0 +1,94 @@ +getValue()); + + $timeStr = ''; + + // Hour + if (!is_null($parts['hour'])) { + $timeStr.=$parts['hour']; + + if (!is_null($parts['minute'])) { + $timeStr.=':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $timeStr.='-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $timeStr.=$parts['minute']; + + if (!is_null($parts['second'])) { + $timeStr.=':'; + } + } else { + if (isset($parts['second'])) { + // Dash for empty minute + $timeStr.='-'; + } + } + + // Second + if (!is_null($parts['second'])) { + $timeStr.=$parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + $timeStr.=$parts['timezone']; + } + + return array($timeStr); + + } + +} diff --git a/3rdparty/VObject/Property/Unknown.php b/3rdparty/VObject/Property/Unknown.php new file mode 100644 index 000000000..177c485ec --- /dev/null +++ b/3rdparty/VObject/Property/Unknown.php @@ -0,0 +1,50 @@ +getRawMimeDirValue()); + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return "UNKNOWN"; + + } + +} diff --git a/3rdparty/VObject/Property/Uri.php b/3rdparty/VObject/Property/Uri.php new file mode 100644 index 000000000..6fd3ed214 --- /dev/null +++ b/3rdparty/VObject/Property/Uri.php @@ -0,0 +1,70 @@ +value = $val; + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + if (is_array($this->value)) { + return $this->value[0]; + } else { + return $this->value; + } + + } + +} diff --git a/3rdparty/VObject/Property/UtcOffset.php b/3rdparty/VObject/Property/UtcOffset.php new file mode 100644 index 000000000..ccc4069fb --- /dev/null +++ b/3rdparty/VObject/Property/UtcOffset.php @@ -0,0 +1,37 @@ +getValue()); + + $dateStr = ''; + + // Year + if (!is_null($parts['year'])) { + $dateStr.=$parts['year']; + + if (!is_null($parts['month'])) { + // If a year and a month is set, we need to insert a separator + // dash. + $dateStr.='-'; + } + + } else { + + if (!is_null($parts['month']) || !is_null($parts['date'])) { + // Inserting two dashes + $dateStr.='--'; + } + + } + + // Month + + if (!is_null($parts['month'])) { + $dateStr.=$parts['month']; + + if (isset($parts['date'])) { + // If month and date are set, we need the separator dash. + $dateStr.='-'; + } + } else { + if (isset($parts['date'])) { + // If the month is empty, and a date is set, we need a 'empty + // dash' + $dateStr.='-'; + } + } + + // Date + if (!is_null($parts['date'])) { + $dateStr.=$parts['date']; + } + + + // Early exit if we don't have a time string. + if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) { + return array($dateStr); + } + + $dateStr.='T'; + + // Hour + if (!is_null($parts['hour'])) { + $dateStr.=$parts['hour']; + + if (!is_null($parts['minute'])) { + $dateStr.=':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $dateStr.='-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $dateStr.=$parts['minute']; + + if (!is_null($parts['second'])) { + $dateStr.=':'; + } + } else { + if (isset($parts['second'])) { + // Dash for empty minute + $dateStr.='-'; + } + } + + // Second + if (!is_null($parts['second'])) { + $dateStr.=$parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + $dateStr.=$parts['timezone']; + } + + return array($dateStr); + + } + +} diff --git a/3rdparty/VObject/Property/VCard/DateTime.php b/3rdparty/VObject/Property/VCard/DateTime.php new file mode 100644 index 000000000..c3f684b09 --- /dev/null +++ b/3rdparty/VObject/Property/VCard/DateTime.php @@ -0,0 +1,33 @@ +setValue($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() { + + return $this->value; + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() { + + return "LANGUAGE-TAG"; + + } + +} diff --git a/3rdparty/VObject/Property/VCard/TimeStamp.php b/3rdparty/VObject/Property/VCard/TimeStamp.php new file mode 100644 index 000000000..f83117be9 --- /dev/null +++ b/3rdparty/VObject/Property/VCard/TimeStamp.php @@ -0,0 +1,69 @@ +getValue()); + + $dateStr = + $parts['year'] . '-' . + $parts['month'] . '-' . + $parts['date'] . 'T' . + $parts['hour'] . ':' . + $parts['minute'] . ':' . + $parts['second']; + + // Timezone + if (!is_null($parts['timezone'])) { + $dateStr.=$parts['timezone']; + } + + return array($dateStr); + + } +} diff --git a/3rdparty/VObject/Reader.php b/3rdparty/VObject/Reader.php new file mode 100644 index 000000000..6a0c81008 --- /dev/null +++ b/3rdparty/VObject/Reader.php @@ -0,0 +1,73 @@ +parse($data, $options); + + return $result; + + } + + /** + * Parses a jCard or jCal object, and returns the top component. + * + * The options argument is a bitfield. Pass any of the OPTIONS constant to + * alter the parsers' behaviour. + * + * You can either a string, a readable stream, or an array for it's input. + * Specifying the array is useful if json_decode was already called on the + * input. + * + * @param string|resource|array $data + * @param int $options + * @return Node + */ + static function readJson($data, $options = 0) { + + $parser = new Parser\Json(); + $result = $parser->parse($data, $options); + + return $result; + + } + +} diff --git a/3rdparty/VObject/RecurrenceIterator.php b/3rdparty/VObject/RecurrenceIterator.php new file mode 100644 index 000000000..b060c59be --- /dev/null +++ b/3rdparty/VObject/RecurrenceIterator.php @@ -0,0 +1,1153 @@ + 0, + 'MO' => 1, + 'TU' => 2, + 'WE' => 3, + 'TH' => 4, + 'FR' => 5, + 'SA' => 6, + ); + + /** + * Mappings between the day number and english day name. + * + * @var array + */ + private $dayNames = array( + 0 => 'Sunday', + 1 => 'Monday', + 2 => 'Tuesday', + 3 => 'Wednesday', + 4 => 'Thursday', + 5 => 'Friday', + 6 => 'Saturday', + ); + + /** + * If the current iteration of the event is an overriden event, this + * property will hold the VObject + * + * @var Component + */ + private $currentOverriddenEvent; + + /** + * This property may contain the date of the next not-overridden event. + * This date is calculated sometimes a bit early, before overridden events + * are evaluated. + * + * @var DateTime + */ + private $nextDate; + + /** + * This counts the number of overridden events we've handled so far + * + * @var int + */ + private $handledOverridden = 0; + + /** + * Creates the iterator + * + * You should pass a VCALENDAR component, as well as the UID of the event + * we're going to traverse. + * + * @param Component $vcal + * @param string|null $uid + */ + public function __construct(Component $vcal, $uid=null) { + + if (is_null($uid)) { + if ($vcal instanceof Component\VCalendar) { + throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well'); + } + $components = array($vcal); + $uid = (string)$vcal->uid; + } else { + $components = $vcal->select('VEVENT'); + } + foreach($components as $component) { + if ((string)$component->uid == $uid) { + if (isset($component->{'RECURRENCE-ID'})) { + $this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component; + $this->overriddenDates[] = $component->{'RECURRENCE-ID'}->getDateTime(); + } else { + $this->baseEvent = $component; + } + } + } + + ksort($this->overriddenEvents); + + if (!$this->baseEvent) { + // No base event was found. CalDAV does allow cases where only + // overridden instances are stored. + // + // In this barticular case, we're just going to grab the first + // event and use that instead. This may not always give the + // desired result. + if (!count($this->overriddenEvents)) { + throw new \InvalidArgumentException('Could not find an event with uid: ' . $uid); + } + ksort($this->overriddenEvents, SORT_NUMERIC); + $this->baseEvent = array_shift($this->overriddenEvents); + } + + $this->startDate = clone $this->baseEvent->DTSTART->getDateTime(); + + $this->endDate = null; + if (isset($this->baseEvent->DTEND)) { + $this->endDate = clone $this->baseEvent->DTEND->getDateTime(); + } else { + $this->endDate = clone $this->startDate; + if (isset($this->baseEvent->DURATION)) { + $this->endDate->add(DateTimeParser::parse((string)$this->baseEvent->DURATION)); + } elseif (!$this->baseEvent->DTSTART->hasTime()) { + $this->endDate->modify('+1 day'); + } + } + $this->currentDate = clone $this->startDate; + + $rrule = $this->baseEvent->RRULE; + + // If no rrule was specified, we create a default setting + if (!$rrule) { + $this->frequency = 'daily'; + $this->count = 1; + } else foreach($rrule->getParts() as $key=>$value) { + + switch($key) { + + case 'FREQ' : + if (!in_array( + strtolower($value), + array('secondly','minutely','hourly','daily','weekly','monthly','yearly') + )) { + throw new \InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value)); + + } + $this->frequency = strtolower($value); + break; + + case 'UNTIL' : + $this->until = DateTimeParser::parse($value); + + // In some cases events are generated with an UNTIL= + // parameter before the actual start of the event. + // + // Not sure why this is happening. We assume that the + // intention was that the event only recurs once. + // + // So we are modifying the parameter so our code doesn't + // break. + if($this->until < $this->baseEvent->DTSTART->getDateTime()) { + $this->until = $this->baseEvent->DTSTART->getDateTime(); + } + break; + + case 'COUNT' : + $this->count = (int)$value; + break; + + case 'INTERVAL' : + $this->interval = (int)$value; + if ($this->interval < 1) { + throw new \InvalidArgumentException('INTERVAL in RRULE must be a positive integer!'); + } + break; + + case 'BYSECOND' : + $this->bySecond = (array)$value; + break; + + case 'BYMINUTE' : + $this->byMinute = (array)$value; + break; + + case 'BYHOUR' : + $this->byHour = (array)$value; + break; + + case 'BYDAY' : + $this->byDay = (array)$value; + break; + + case 'BYMONTHDAY' : + $this->byMonthDay = (array)$value; + break; + + case 'BYYEARDAY' : + $this->byYearDay = (array)$value; + break; + + case 'BYWEEKNO' : + $this->byWeekNo = (array)$value; + break; + + case 'BYMONTH' : + $this->byMonth = (array)$value; + break; + + case 'BYSETPOS' : + $this->bySetPos = (array)$value; + break; + + case 'WKST' : + $this->weekStart = strtoupper($value); + break; + + } + + } + + // Parsing exception dates + if (isset($this->baseEvent->EXDATE)) { + foreach($this->baseEvent->EXDATE as $exDate) { + + foreach(explode(',', (string)$exDate) as $exceptionDate) { + + $this->exceptionDates[] = + DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone()); + + } + + } + + } + + } + + /** + * Returns the current item in the list + * + * @return DateTime + */ + public function current() { + + if (!$this->valid()) return null; + return clone $this->currentDate; + + } + + /** + * This method returns the startdate for the current iteration of the + * event. + * + * @return DateTime + */ + public function getDtStart() { + + if (!$this->valid()) return null; + return clone $this->currentDate; + + } + + /** + * This method returns the enddate for the current iteration of the + * event. + * + * @return DateTime + */ + public function getDtEnd() { + + if (!$this->valid()) return null; + $dtEnd = clone $this->currentDate; + $dtEnd->add( $this->startDate->diff( $this->endDate ) ); + return clone $dtEnd; + + } + + /** + * Returns a VEVENT object with the updated start and end date. + * + * Any recurrence information is removed, and this function may return an + * 'overridden' event instead. + * + * This method always returns a cloned instance. + * + * @return Component\VEvent + */ + public function getEventObject() { + + if ($this->currentOverriddenEvent) { + return clone $this->currentOverriddenEvent; + } + $event = clone $this->baseEvent; + unset($event->RRULE); + unset($event->EXDATE); + unset($event->RDATE); + unset($event->EXRULE); + + $event->DTSTART->setDateTime($this->getDTStart()); + if (isset($event->DTEND)) { + $event->DTEND->setDateTime($this->getDtEnd()); + } + if ($this->counter > 0) { + $event->{'RECURRENCE-ID'} = (string)$event->DTSTART; + } + + return $event; + + } + + /** + * Returns the current item number + * + * @return int + */ + public function key() { + + return $this->counter; + + } + + /** + * Whether or not there is a 'next item' + * + * @return bool + */ + public function valid() { + + if (!is_null($this->count)) { + return $this->counter < $this->count; + } + if (!is_null($this->until) && $this->currentDate > $this->until) { + + // Need to make sure there's no overridden events past the + // until date. + foreach($this->overriddenEvents as $overriddenEvent) { + + if ($overriddenEvent->DTSTART->getDateTime() >= $this->currentDate) { + + return true; + } + } + return false; + } + return true; + + } + + /** + * Resets the iterator + * + * @return void + */ + public function rewind() { + + $this->currentDate = clone $this->startDate; + $this->counter = 0; + + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + * + * Note that this checks the current 'endDate', not the 'stardDate'. This + * means that if you forward to January 1st, the iterator will stop at the + * first event that ends *after* January 1st. + * + * @param DateTime $dt + * @return void + */ + public function fastForward(\DateTime $dt) { + + while($this->valid() && $this->getDTEnd() <= $dt) { + $this->next(); + } + + } + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() { + + return !$this->count && !$this->until; + + } + + /** + * Goes on to the next iteration + * + * @return void + */ + public function next() { + + $previousStamp = $this->currentDate->getTimeStamp(); + + // Finding the next overridden event in line, and storing that for + // later use. + $overriddenEvent = null; + $overriddenDate = null; + $this->currentOverriddenEvent = null; + + foreach($this->overriddenEvents as $index=>$event) { + if ($index > $previousStamp) { + $overriddenEvent = $event; + $overriddenDate = clone $event->DTSTART->getDateTime(); + break; + } + } + + // If we have a stored 'next date', we will use that. + if ($this->nextDate) { + if (!$overriddenDate || $this->nextDate < $overriddenDate) { + $this->currentDate = $this->nextDate; + $currentStamp = $this->currentDate->getTimeStamp(); + $this->nextDate = null; + } else { + $this->currentDate = clone $overriddenDate; + $this->currentOverriddenEvent = $overriddenEvent; + } + $this->counter++; + return; + } + + while(true) { + + // Otherwise, we find the next event in the normal RRULE + // sequence. + switch($this->frequency) { + + case 'hourly' : + $this->nextHourly(); + break; + + case 'daily' : + $this->nextDaily(); + break; + + case 'weekly' : + $this->nextWeekly(); + break; + + case 'monthly' : + $this->nextMonthly(); + break; + + case 'yearly' : + $this->nextYearly(); + break; + + } + $currentStamp = $this->currentDate->getTimeStamp(); + + + // Checking exception dates + foreach($this->exceptionDates as $exceptionDate) { + if ($this->currentDate == $exceptionDate) { + $this->counter++; + continue 2; + } + } + foreach($this->overriddenDates as $check) { + if ($this->currentDate == $check) { + continue 2; + } + } + break; + + } + + + + // Is the date we have actually higher than the next overiddenEvent? + if ($overriddenDate && $this->currentDate > $overriddenDate) { + $this->nextDate = clone $this->currentDate; + $this->currentDate = clone $overriddenDate; + $this->currentOverriddenEvent = $overriddenEvent; + $this->handledOverridden++; + } + $this->counter++; + + + /* + * If we have overridden events left in the queue, but our counter is + * running out, we should grab one of those. + */ + if (!is_null($overriddenEvent) && !is_null($this->count) && count($this->overriddenEvents) - $this->handledOverridden >= ($this->count - $this->counter)) { + + $this->currentOverriddenEvent = $overriddenEvent; + $this->currentDate = clone $overriddenDate; + $this->handledOverridden++; + + } + + } + + /** + * Does the processing for advancing the iterator for hourly frequency. + * + * @return void + */ + protected function nextHourly() { + + if (!$this->byHour) { + $this->currentDate->modify('+' . $this->interval . ' hours'); + return; + } + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + + /** + * Does the processing for advancing the iterator for daily frequency. + * + * @return void + */ + protected function nextDaily() { + + if (!$this->byHour && !$this->byDay) { + $this->currentDate->modify('+' . $this->interval . ' days'); + return; + } + + if (isset($this->byHour)) { + $recurrenceHours = $this->getHours(); + } + + if (isset($this->byDay)) { + $recurrenceDays = $this->getDays(); + } + + do { + + if ($this->byHour) { + if ($this->currentDate->format('G') == '23') { + // to obey the interval rule + $this->currentDate->modify('+' . $this->interval-1 . ' days'); + } + + $this->currentDate->modify('+1 hours'); + + } else { + $this->currentDate->modify('+' . $this->interval . ' days'); + + } + + // Current day of the week + $currentDay = $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = $this->currentDate->format('G'); + + } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); + + } + + /** + * Does the processing for advancing the iterator for weekly frequency. + * + * @return void + */ + protected function nextWeekly() { + + if (!$this->byHour && !$this->byDay) { + $this->currentDate->modify('+' . $this->interval . ' weeks'); + return; + } + + if ($this->byHour) { + $recurrenceHours = $this->getHours(); + } + + if ($this->byDay) { + $recurrenceDays = $this->getDays(); + } + + // First day of the week: + $firstDay = $this->dayMap[$this->weekStart]; + + do { + + if ($this->byHour) { + $this->currentDate->modify('+1 hours'); + } else { + $this->currentDate->modify('+1 days'); + } + + // Current day of the week + $currentDay = (int) $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = (int) $this->currentDate->format('G'); + + // We need to roll over to the next week + if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) { + $this->currentDate->modify('+' . $this->interval-1 . ' weeks'); + + // We need to go to the first day of this week, but only if we + // are not already on this first day of this week. + if($this->currentDate->format('w') != $firstDay) { + $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]); + } + } + + // We have a match + } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); + } + + /** + * Does the processing for advancing the iterator for monthly frequency. + * + * @return void + */ + protected function nextMonthly() { + + $currentDayOfMonth = $this->currentDate->format('j'); + if (!$this->byMonthDay && !$this->byDay) { + + // If the current day is higher than the 28th, rollover can + // occur to the next month. We Must skip these invalid + // entries. + if ($currentDayOfMonth < 29) { + $this->currentDate->modify('+' . $this->interval . ' months'); + } else { + $increase = 0; + do { + $increase++; + $tempDate = clone $this->currentDate; + $tempDate->modify('+ ' . ($this->interval*$increase) . ' months'); + } while ($tempDate->format('j') != $currentDayOfMonth); + $this->currentDate = $tempDate; + } + return; + } + + while(true) { + + $occurrences = $this->getMonthlyOccurrences(); + + foreach($occurrences as $occurrence) { + + // The first occurrence thats higher than the current + // day of the month wins. + if ($occurrence > $currentDayOfMonth) { + break 2; + } + + } + + // If we made it all the way here, it means there were no + // valid occurrences, and we need to advance to the next + // month. + $this->currentDate->modify('first day of this month'); + $this->currentDate->modify('+ ' . $this->interval . ' months'); + + // This goes to 0 because we need to start counting at hte + // beginning. + $currentDayOfMonth = 0; + + } + + $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence); + + } + + /** + * Does the processing for advancing the iterator for yearly frequency. + * + * @return void + */ + protected function nextYearly() { + + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + // No sub-rules, so we just advance by year + if (!$this->byMonth) { + + // Unless it was a leap day! + if ($currentMonth==2 && $currentDayOfMonth==29) { + + $counter = 0; + do { + $counter++; + // Here we increase the year count by the interval, until + // we hit a date that's also in a leap year. + // + // We could just find the next interval that's dividable by + // 4, but that would ignore the rule that there's no leap + // year every year that's dividable by a 100, but not by + // 400. (1800, 1900, 2100). So we just rely on the datetime + // functions instead. + $nextDate = clone $this->currentDate; + $nextDate->modify('+ ' . ($this->interval*$counter) . ' years'); + } while ($nextDate->format('n')!=2); + $this->currentDate = $nextDate; + + return; + + } + + // The easiest form + $this->currentDate->modify('+' . $this->interval . ' years'); + return; + + } + + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + $advancedToNewMonth = false; + + // If we got a byDay or getMonthDay filter, we must first expand + // further. + if ($this->byDay || $this->byMonthDay) { + + while(true) { + + $occurrences = $this->getMonthlyOccurrences(); + + foreach($occurrences as $occurrence) { + + // The first occurrence that's higher than the current + // day of the month wins. + // If we advanced to the next month or year, the first + // occurrence is always correct. + if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { + break 2; + } + + } + + // If we made it here, it means we need to advance to + // the next month or year. + $currentDayOfMonth = 1; + $advancedToNewMonth = true; + do { + + $currentMonth++; + if ($currentMonth>12) { + $currentYear+=$this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + + $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); + + } + + // If we made it here, it means we got a valid occurrence + $this->currentDate->setDate($currentYear, $currentMonth, $occurrence); + return; + + } else { + + // These are the 'byMonth' rules, if there are no byDay or + // byMonthDay sub-rules. + do { + + $currentMonth++; + if ($currentMonth>12) { + $currentYear+=$this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); + + return; + + } + + } + + /** + * Returns all the occurrences for a monthly frequency with a 'byDay' or + * 'byMonthDay' expansion for the current month. + * + * The returned list is an array of integers with the day of month (1-31). + * + * @return array + */ + protected function getMonthlyOccurrences() { + + $startDate = clone $this->currentDate; + + $byDayResults = array(); + + // Our strategy is to simply go through the byDays, advance the date to + // that point and add it to the results. + if ($this->byDay) foreach($this->byDay as $day) { + + $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]]; + + // Dayname will be something like 'wednesday'. Now we need to find + // all wednesdays in this month. + $dayHits = array(); + + $checkDate = clone $startDate; + $checkDate->modify('first day of this month'); + $checkDate->modify($dayName); + + do { + $dayHits[] = $checkDate->format('j'); + $checkDate->modify('next ' . $dayName); + } while ($checkDate->format('n') === $startDate->format('n')); + + // So now we have 'all wednesdays' for month. It is however + // possible that the user only really wanted the 1st, 2nd or last + // wednesday. + if (strlen($day)>2) { + $offset = (int)substr($day,0,-2); + + if ($offset>0) { + // It is possible that the day does not exist, such as a + // 5th or 6th wednesday of the month. + if (isset($dayHits[$offset-1])) { + $byDayResults[] = $dayHits[$offset-1]; + } + } else { + + // if it was negative we count from the end of the array + $byDayResults[] = $dayHits[count($dayHits) + $offset]; + } + } else { + // There was no counter (first, second, last wednesdays), so we + // just need to add the all to the list). + $byDayResults = array_merge($byDayResults, $dayHits); + + } + + } + + $byMonthDayResults = array(); + if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) { + + // Removing values that are out of range for this month + if ($monthDay > $startDate->format('t') || + $monthDay < 0-$startDate->format('t')) { + continue; + } + if ($monthDay>0) { + $byMonthDayResults[] = $monthDay; + } else { + // Negative values + $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; + } + } + + // If there was just byDay or just byMonthDay, they just specify our + // (almost) final list. If both were provided, then byDay limits the + // list. + if ($this->byMonthDay && $this->byDay) { + $result = array_intersect($byMonthDayResults, $byDayResults); + } elseif ($this->byMonthDay) { + $result = $byMonthDayResults; + } else { + $result = $byDayResults; + } + $result = array_unique($result); + sort($result, SORT_NUMERIC); + + // The last thing that needs checking is the BYSETPOS. If it's set, it + // means only certain items in the set survive the filter. + if (!$this->bySetPos) { + return $result; + } + + $filteredResult = array(); + foreach($this->bySetPos as $setPos) { + + if ($setPos<0) { + $setPos = count($result)-($setPos+1); + } + if (isset($result[$setPos-1])) { + $filteredResult[] = $result[$setPos-1]; + } + } + + sort($filteredResult, SORT_NUMERIC); + return $filteredResult; + + } + + protected function getHours() + { + $recurrenceHours = array(); + foreach($this->byHour as $byHour) { + $recurrenceHours[] = $byHour; + } + + return $recurrenceHours; + } + + protected function getDays() + { + $recurrenceDays = array(); + foreach($this->byDay as $byDay) { + + // The day may be preceeded with a positive (+n) or + // negative (-n) integer. However, this does not make + // sense in 'weekly' so we ignore it here. + $recurrenceDays[] = $this->dayMap[substr($byDay,-2)]; + + } + + return $recurrenceDays; + } +} + diff --git a/3rdparty/VObject/Splitter/ICalendar.php b/3rdparty/VObject/Splitter/ICalendar.php new file mode 100644 index 000000000..02c69487f --- /dev/null +++ b/3rdparty/VObject/Splitter/ICalendar.php @@ -0,0 +1,114 @@ +children() as $component) { + if (!$component instanceof VObject\Component) { + continue; + } + + // Get all timezones + if ($component->name === 'VTIMEZONE') { + $this->vtimezones[(string)$component->TZID] = $component; + continue; + } + + // Get component UID for recurring Events search + if($component->UID) { + $uid = (string)$component->UID; + } else { + // Generating a random UID + $uid = sha1(microtime()) . '-vobjectimport'; + } + + // Take care of recurring events + if (!array_key_exists($uid, $this->objects)) { + $this->objects[$uid] = new VCalendar(); + } + + $this->objects[$uid]->add(clone $component); + } + + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @return Sabre\VObject\Component|null + */ + public function getNext() { + + if($object=array_shift($this->objects)) { + + // create our baseobject + $object->version = '2.0'; + $object->prodid = '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN'; + $object->calscale = 'GREGORIAN'; + + // add vtimezone information to obj (if we have it) + foreach ($this->vtimezones as $vtimezone) { + $object->add($vtimezone); + } + + return $object; + + } else { + + return null; + + } + + } + +} diff --git a/3rdparty/VObject/Splitter/SplitterInterface.php b/3rdparty/VObject/Splitter/SplitterInterface.php new file mode 100644 index 000000000..7d740621e --- /dev/null +++ b/3rdparty/VObject/Splitter/SplitterInterface.php @@ -0,0 +1,39 @@ +input = $input; + $this->parser = new MimeDir($input, $options); + + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @return Sabre\VObject\Component|null + */ + public function getNext() { + + try { + $object = $this->parser->parse(); + } catch (VObject\EofException $e) { + return null; + } + + return $object; + + } + +} diff --git a/3rdparty/VObject/StringUtil.php b/3rdparty/VObject/StringUtil.php new file mode 100644 index 000000000..b39debd48 --- /dev/null +++ b/3rdparty/VObject/StringUtil.php @@ -0,0 +1,61 @@ +'Australia/Darwin', + 'AUS Eastern Standard Time'=>'Australia/Sydney', + 'Afghanistan Standard Time'=>'Asia/Kabul', + 'Alaskan Standard Time'=>'America/Anchorage', + 'Arab Standard Time'=>'Asia/Riyadh', + 'Arabian Standard Time'=>'Asia/Dubai', + 'Arabic Standard Time'=>'Asia/Baghdad', + 'Argentina Standard Time'=>'America/Buenos_Aires', + 'Armenian Standard Time'=>'Asia/Yerevan', + 'Atlantic Standard Time'=>'America/Halifax', + 'Azerbaijan Standard Time'=>'Asia/Baku', + 'Azores Standard Time'=>'Atlantic/Azores', + 'Bangladesh Standard Time'=>'Asia/Dhaka', + 'Canada Central Standard Time'=>'America/Regina', + 'Cape Verde Standard Time'=>'Atlantic/Cape_Verde', + 'Caucasus Standard Time'=>'Asia/Yerevan', + 'Cen. Australia Standard Time'=>'Australia/Adelaide', + 'Central America Standard Time'=>'America/Guatemala', + 'Central Asia Standard Time'=>'Asia/Almaty', + 'Central Brazilian Standard Time'=>'America/Cuiaba', + 'Central Europe Standard Time'=>'Europe/Budapest', + 'Central European Standard Time'=>'Europe/Warsaw', + 'Central Pacific Standard Time'=>'Pacific/Guadalcanal', + 'Central Standard Time'=>'America/Chicago', + 'Central Standard Time (Mexico)'=>'America/Mexico_City', + 'China Standard Time'=>'Asia/Shanghai', + 'Dateline Standard Time'=>'Etc/GMT+12', + 'E. Africa Standard Time'=>'Africa/Nairobi', + 'E. Australia Standard Time'=>'Australia/Brisbane', + 'E. Europe Standard Time'=>'Europe/Minsk', + 'E. South America Standard Time'=>'America/Sao_Paulo', + 'Eastern Standard Time'=>'America/New_York', + 'Egypt Standard Time'=>'Africa/Cairo', + 'Ekaterinburg Standard Time'=>'Asia/Yekaterinburg', + 'FLE Standard Time'=>'Europe/Kiev', + 'Fiji Standard Time'=>'Pacific/Fiji', + 'GMT Standard Time'=>'Europe/London', + 'GTB Standard Time'=>'Europe/Istanbul', + 'Georgian Standard Time'=>'Asia/Tbilisi', + 'Greenland Standard Time'=>'America/Godthab', + 'Greenwich Standard Time'=>'Atlantic/Reykjavik', + 'Hawaiian Standard Time'=>'Pacific/Honolulu', + 'India Standard Time'=>'Asia/Calcutta', + 'Iran Standard Time'=>'Asia/Tehran', + 'Israel Standard Time'=>'Asia/Jerusalem', + 'Jordan Standard Time'=>'Asia/Amman', + 'Kamchatka Standard Time'=>'Asia/Kamchatka', + 'Korea Standard Time'=>'Asia/Seoul', + 'Magadan Standard Time'=>'Asia/Magadan', + 'Mauritius Standard Time'=>'Indian/Mauritius', + 'Mexico Standard Time'=>'America/Mexico_City', + 'Mexico Standard Time 2'=>'America/Chihuahua', + 'Mid-Atlantic Standard Time'=>'Etc/GMT-2', + 'Middle East Standard Time'=>'Asia/Beirut', + 'Montevideo Standard Time'=>'America/Montevideo', + 'Morocco Standard Time'=>'Africa/Casablanca', + 'Mountain Standard Time'=>'America/Denver', + 'Mountain Standard Time (Mexico)'=>'America/Chihuahua', + 'Myanmar Standard Time'=>'Asia/Rangoon', + 'N. Central Asia Standard Time'=>'Asia/Novosibirsk', + 'Namibia Standard Time'=>'Africa/Windhoek', + 'Nepal Standard Time'=>'Asia/Katmandu', + 'New Zealand Standard Time'=>'Pacific/Auckland', + 'Newfoundland Standard Time'=>'America/St_Johns', + 'North Asia East Standard Time'=>'Asia/Irkutsk', + 'North Asia Standard Time'=>'Asia/Krasnoyarsk', + 'Pacific SA Standard Time'=>'America/Santiago', + 'Pacific Standard Time'=>'America/Los_Angeles', + 'Pacific Standard Time (Mexico)'=>'America/Santa_Isabel', + 'Pakistan Standard Time'=>'Asia/Karachi', + 'Paraguay Standard Time'=>'America/Asuncion', + 'Romance Standard Time'=>'Europe/Paris', + 'Russian Standard Time'=>'Europe/Moscow', + 'SA Eastern Standard Time'=>'America/Cayenne', + 'SA Pacific Standard Time'=>'America/Bogota', + 'SA Western Standard Time'=>'America/La_Paz', + 'SE Asia Standard Time'=>'Asia/Bangkok', + 'Samoa Standard Time'=>'Pacific/Apia', + 'Singapore Standard Time'=>'Asia/Singapore', + 'South Africa Standard Time'=>'Africa/Johannesburg', + 'Sri Lanka Standard Time'=>'Asia/Colombo', + 'Syria Standard Time'=>'Asia/Damascus', + 'Taipei Standard Time'=>'Asia/Taipei', + 'Tasmania Standard Time'=>'Australia/Hobart', + 'Tokyo Standard Time'=>'Asia/Tokyo', + 'Tonga Standard Time'=>'Pacific/Tongatapu', + 'US Eastern Standard Time'=>'America/Indianapolis', + 'US Mountain Standard Time'=>'America/Phoenix', + 'UTC+12'=>'Etc/GMT-12', + 'UTC-02'=>'Etc/GMT+2', + 'UTC-11'=>'Etc/GMT+11', + 'Ulaanbaatar Standard Time'=>'Asia/Ulaanbaatar', + 'Venezuela Standard Time'=>'America/Caracas', + 'Vladivostok Standard Time'=>'Asia/Vladivostok', + 'W. Australia Standard Time'=>'Australia/Perth', + 'W. Central Africa Standard Time'=>'Africa/Lagos', + 'W. Europe Standard Time'=>'Europe/Berlin', + 'West Asia Standard Time'=>'Asia/Tashkent', + 'West Pacific Standard Time'=>'Pacific/Port_Moresby', + 'Yakutsk Standard Time'=>'Asia/Yakutsk', + + // Microsoft exchange timezones + // Source: + // http://msdn.microsoft.com/en-us/library/ms988620%28v=exchg.65%29.aspx + // + // Correct timezones deduced with help from: + // http://en.wikipedia.org/wiki/List_of_tz_database_time_zones + 'Universal Coordinated Time' => 'UTC', + 'Casablanca, Monrovia' => 'Africa/Casablanca', + 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon', + 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', + 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', + 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', + 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', + 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', + 'Prague, Central Europe' => 'Europe/Prague', + 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo', + 'West Central Africa' => 'Africa/Luanda', // This was a best guess + 'Athens, Istanbul, Minsk' => 'Europe/Athens', + 'Bucharest' => 'Europe/Bucharest', + 'Cairo' => 'Africa/Cairo', + 'Harare, Pretoria' => 'Africa/Harare', + 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki', + 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem', + 'Baghdad' => 'Asia/Baghdad', + 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait', + 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', + 'East Africa, Nairobi' => 'Africa/Nairobi', + 'Tehran' => 'Asia/Tehran', + 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess + 'Baku, Tbilisi, Yerevan' => 'Asia/Baku', + 'Kabul' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', + 'Kathmandu, Nepal' => 'Asia/Kathmandu', + 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', + 'Astana, Dhaka' => 'Asia/Dhaka', + 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', + 'Rangoon' => 'Asia/Rangoon', + 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', + 'Krasnoyarsk' => 'Asia/Krasnoyarsk', + 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', + 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk', + 'Kuala Lumpur, Singapore' => 'Asia/Singapore', + 'Perth, Western Australia' => 'Australia/Perth', + 'Taipei' => 'Asia/Taipei', + 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo', + 'Seoul, Korea Standard time' => 'Asia/Seoul', + 'Yakutsk' => 'Asia/Yakutsk', + 'Adelaide, Central Australia' => 'Australia/Adelaide', + 'Darwin' => 'Australia/Darwin', + 'Brisbane, East Australia' => 'Australia/Brisbane', + 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney', + 'Guam, Port Moresby' => 'Pacific/Guam', + 'Hobart, Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan', + 'Auckland, Wellington' => 'Pacific/Auckland', + 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji', + 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu', + 'Azores' => 'Atlantic/Azores', + 'Cape Verde Is.' => 'Atlantic/Cape_Verde', + 'Mid-Atlantic' => 'America/Noronha', + 'Brasilia' => 'America/Sao_Paulo', // Best guess + 'Buenos Aires' => 'America/Argentina/Buenos_Aires', + 'Greenland' => 'America/Godthab', + 'Newfoundland' => 'America/St_Johns', + 'Atlantic Time (Canada)' => 'America/Halifax', + 'Caracas, La Paz' => 'America/Caracas', + 'Santiago' => 'America/Santiago', + 'Bogota, Lima, Quito' => 'America/Bogota', + 'Eastern Time (US & Canada)' => 'America/New_York', + 'Indiana (East)' => 'America/Indiana/Indianapolis', + 'Central America' => 'America/Guatemala', + 'Central Time (US & Canada)' => 'America/Chicago', + 'Mexico City, Tegucigalpa' => 'America/Mexico_City', + 'Saskatchewan' => 'America/Edmonton', + 'Arizona' => 'America/Phoenix', + 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess + 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess + 'Alaska' => 'America/Anchorage', + 'Hawaii' => 'Pacific/Honolulu', + 'Midway Island, Samoa' => 'Pacific/Midway', + 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', + + // The following list are timezone names that could be generated by + // Lotus / Domino + 'Dateline' => 'Etc/GMT-12', + 'Samoa' => 'Pacific/Apia', + 'Hawaiian' => 'Pacific/Honolulu', + 'Alaskan' => 'America/Anchorage', + 'Pacific' => 'America/Los_Angeles', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Mexico Standard Time 2' => 'America/Chihuahua', + 'Mountain' => 'America/Denver', + 'Mountain Standard Time' => 'America/Chihuahua', + 'US Mountain' => 'America/Phoenix', + 'Canada Central' => 'America/Edmonton', + 'Central America' => 'America/Guatemala', + 'Central' => 'America/Chicago', + 'Central Standard Time' => 'America/Mexico_City', + 'Mexico' => 'America/Mexico_City', + 'Eastern' => 'America/New_York', + 'SA Pacific' => 'America/Bogota', + 'US Eastern' => 'America/Indiana/Indianapolis', + 'Venezuela' => 'America/Caracas', + 'Atlantic' => 'America/Halifax', + 'Central Brazilian' => 'America/Manaus', + 'Pacific SA' => 'America/Santiago', + 'SA Western' => 'America/La_Paz', + 'Newfoundland' => 'America/St_Johns', + 'Argentina' => 'America/Argentina/Buenos_Aires', + 'E. South America' => 'America/Belem', + 'Greenland' => 'America/Godthab', + 'Montevideo' => 'America/Montevideo', + 'SA Eastern' => 'America/Belem', + 'Mid-Atlantic' => 'Etc/GMT-2', + 'Azores' => 'Atlantic/Azores', + 'Cape Verde' => 'Atlantic/Cape_Verde', + 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT. + 'Morocco' => 'Africa/Casablanca', + 'Central Europe' => 'Europe/Prague', + 'Central European' => 'Europe/Sarajevo', + 'Romance' => 'Europe/Paris', + 'W. Central Africa' => 'Africa/Lagos', // Best guess + 'W. Europe' => 'Europe/Amsterdam', + 'E. Europe' => 'Europe/Minsk', + 'Egypt' => 'Africa/Cairo', + 'FLE' => 'Europe/Helsinki', + 'GTB' => 'Europe/Athens', + 'Israel' => 'Asia/Jerusalem', + 'Jordan' => 'Asia/Amman', + 'Middle East' => 'Asia/Beirut', + 'Namibia' => 'Africa/Windhoek', + 'South Africa' => 'Africa/Harare', + 'Arab' => 'Asia/Kuwait', + 'Arabic' => 'Asia/Baghdad', + 'E. Africa' => 'Africa/Nairobi', + 'Georgian' => 'Asia/Tbilisi', + 'Russian' => 'Europe/Moscow', + 'Iran' => 'Asia/Tehran', + 'Arabian' => 'Asia/Muscat', + 'Armenian' => 'Asia/Yerevan', + 'Azerbijan' => 'Asia/Baku', + 'Caucasus' => 'Asia/Yerevan', + 'Mauritius' => 'Indian/Mauritius', + 'Afghanistan' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Pakistan' => 'Asia/Karachi', + 'West Asia' => 'Asia/Tashkent', + 'India' => 'Asia/Calcutta', + 'Sri Lanka' => 'Asia/Colombo', + 'Nepal' => 'Asia/Kathmandu', + 'Central Asia' => 'Asia/Dhaka', + 'N. Central Asia' => 'Asia/Almaty', + 'Myanmar' => 'Asia/Rangoon', + 'North Asia' => 'Asia/Krasnoyarsk', + 'SE Asia' => 'Asia/Bangkok', + 'China' => 'Asia/Shanghai', + 'North Asia East' => 'Asia/Irkutsk', + 'Singapore' => 'Asia/Singapore', + 'Taipei' => 'Asia/Taipei', + 'W. Australia' => 'Australia/Perth', + 'Korea' => 'Asia/Seoul', + 'Tokyo' => 'Asia/Tokyo', + 'Yakutsk' => 'Asia/Yakutsk', + 'AUS Central' => 'Australia/Darwin', + 'Cen. Australia' => 'Australia/Adelaide', + 'AUS Eastern' => 'Australia/Sydney', + 'E. Australia' => 'Australia/Brisbane', + 'Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'West Pacific' => 'Pacific/Guam', + 'Central Pacific' => 'Asia/Magadan', + 'Fiji' => 'Pacific/Fiji', + 'New Zealand' => 'Pacific/Auckland', + 'Tonga' => 'Pacific/Tongatapu', + + // PHP 5.5.10 failed on a few timezones that were valid before. We're + // normalizing them here. + 'CST6CDT' => 'America/Chicago', + 'Cuba' => 'America/Havana', + 'Egypt' => 'Africa/Cairo', + 'Eire' => 'Europe/Dublin', + 'EST5EDT' => 'America/New_York', + 'Factory' => 'UTC', + 'GB-Eire' => 'Europe/London', + 'GMT0' => 'UTC', + 'Greenwich' => 'UTC', + 'Hongkong' => 'Asia/Hong_Kong', + 'Iceland' => 'Atlantic/Reykjavik', + 'Iran' => 'Asia/Tehran', + 'Israel' => 'Asia/Jerusalem', + 'Jamaica' => 'America/Jamaica', + 'Japan' => 'Asia/Tokyo', + 'Kwajalein' => 'Pacific/Kwajalein', + 'Libya' => 'Africa/Tripoli', + 'MST7MDT' => 'America/Denver', + 'Navajo' => 'America/Denver', + 'NZ-CHAT' => 'Pacific/Chatham', + 'Poland' => 'Europe/Warsaw', + 'Portugal' => 'Europe/Lisbon', + 'PST8PDT' => 'America/Los_Angeles', + 'Singapore' => 'Asia/Singapore', + 'Turkey' => 'Europe/Istanbul', + 'Universal' => 'UTC', + 'W-SU' => 'Europe/Moscow', + ); + + /** + * List of microsoft exchange timezone ids. + * + * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx + */ + public static $microsoftExchangeMap = array( + 0 => 'UTC', + 31 => 'Africa/Casablanca', + + // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. + // I'm not even kidding.. We handle this special case in the + // getTimeZone method. + 2 => 'Europe/Lisbon', + 1 => 'Europe/London', + 4 => 'Europe/Berlin', + 6 => 'Europe/Prague', + 3 => 'Europe/Paris', + 69 => 'Africa/Luanda', // This was a best guess + 7 => 'Europe/Athens', + 5 => 'Europe/Bucharest', + 49 => 'Africa/Cairo', + 50 => 'Africa/Harare', + 59 => 'Europe/Helsinki', + 27 => 'Asia/Jerusalem', + 26 => 'Asia/Baghdad', + 74 => 'Asia/Kuwait', + 51 => 'Europe/Moscow', + 56 => 'Africa/Nairobi', + 25 => 'Asia/Tehran', + 24 => 'Asia/Muscat', // Best guess + 54 => 'Asia/Baku', + 48 => 'Asia/Kabul', + 58 => 'Asia/Yekaterinburg', + 47 => 'Asia/Karachi', + 23 => 'Asia/Calcutta', + 62 => 'Asia/Kathmandu', + 46 => 'Asia/Almaty', + 71 => 'Asia/Dhaka', + 66 => 'Asia/Colombo', + 61 => 'Asia/Rangoon', + 22 => 'Asia/Bangkok', + 64 => 'Asia/Krasnoyarsk', + 45 => 'Asia/Shanghai', + 63 => 'Asia/Irkutsk', + 21 => 'Asia/Singapore', + 73 => 'Australia/Perth', + 75 => 'Asia/Taipei', + 20 => 'Asia/Tokyo', + 72 => 'Asia/Seoul', + 70 => 'Asia/Yakutsk', + 19 => 'Australia/Adelaide', + 44 => 'Australia/Darwin', + 18 => 'Australia/Brisbane', + 76 => 'Australia/Sydney', + 43 => 'Pacific/Guam', + 42 => 'Australia/Hobart', + 68 => 'Asia/Vladivostok', + 41 => 'Asia/Magadan', + 17 => 'Pacific/Auckland', + 40 => 'Pacific/Fiji', + 67 => 'Pacific/Tongatapu', + 29 => 'Atlantic/Azores', + 53 => 'Atlantic/Cape_Verde', + 30 => 'America/Noronha', + 8 => 'America/Sao_Paulo', // Best guess + 32 => 'America/Argentina/Buenos_Aires', + 60 => 'America/Godthab', + 28 => 'America/St_Johns', + 9 => 'America/Halifax', + 33 => 'America/Caracas', + 65 => 'America/Santiago', + 35 => 'America/Bogota', + 10 => 'America/New_York', + 34 => 'America/Indiana/Indianapolis', + 55 => 'America/Guatemala', + 11 => 'America/Chicago', + 37 => 'America/Mexico_City', + 36 => 'America/Edmonton', + 38 => 'America/Phoenix', + 12 => 'America/Denver', // Best guess + 13 => 'America/Los_Angeles', // Best guess + 14 => 'America/Anchorage', + 15 => 'Pacific/Honolulu', + 16 => 'Pacific/Midway', + 39 => 'Pacific/Kwajalein', + ); + + /** + * This method will try to find out the correct timezone for an iCalendar + * date-time value. + * + * You must pass the contents of the TZID parameter, as well as the full + * calendar. + * + * If the lookup fails, this method will return the default PHP timezone + * (as configured using date_default_timezone_set, or the date.timezone ini + * setting). + * + * Alternatively, if $failIfUncertain is set to true, it will throw an + * exception if we cannot accurately determine the timezone. + * + * @param string $tzid + * @param Sabre\VObject\Component $vcalendar + * @return DateTimeZone + */ + static public function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) { + + // First we will just see if the tzid is a support timezone identifier. + // + // The only exception is if the timezone starts with (. This is to + // handle cases where certain microsoft products generate timezone + // identifiers that for instance look like: + // + // (GMT+01.00) Sarajevo/Warsaw/Zagreb + // + // Since PHP 5.5.10, the first bit will be used as the timezone and + // this method will return just GMT+01:00. This is wrong, because it + // doesn't take DST into account. + if ($tzid[0]!=='(') { + try { + return new \DateTimeZone($tzid); + } catch (\Exception $e) { + } + } + + // Next, we check if the tzid is somewhere in our tzid map. + if (isset(self::$map[$tzid])) { + return new \DateTimeZone(self::$map[$tzid]); + } + + // Maybe the author was hyper-lazy and just included an offset. We + // support it, but we aren't happy about it. + // + // Note that the path in the source will never be taken from PHP 5.5.10 + // onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it + // already gets returned early in this function. Once we drop support + // for versions under PHP 5.5.10, this bit can be taken out of the + // source. + if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) { + return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2],0,2),'0')); + } + + if ($vcalendar) { + + // If that didn't work, we will scan VTIMEZONE objects + foreach($vcalendar->select('VTIMEZONE') as $vtimezone) { + + if ((string)$vtimezone->TZID === $tzid) { + + // Some clients add 'X-LIC-LOCATION' with the olson name. + if (isset($vtimezone->{'X-LIC-LOCATION'})) { + + $lic = (string)$vtimezone->{'X-LIC-LOCATION'}; + + // Libical generators may specify strings like + // "SystemV/EST5EDT". For those we must remove the + // SystemV part. + if (substr($lic,0,8)==='SystemV/') { + $lic = substr($lic,8); + } + + return self::getTimeZone($lic, null, $failIfUncertain); + + } + // Microsoft may add a magic number, which we also have an + // answer for. + if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { + $cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); + + // 2 can mean both Europe/Lisbon and Europe/Sarajevo. + if ($cdoId===2 && strpos((string)$vtimezone->TZID, 'Sarajevo')!==false) { + return new \DateTimeZone('Europe/Sarajevo'); + } + + if (isset(self::$microsoftExchangeMap[$cdoId])) { + return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]); + } + } + + } + + } + + } + + if ($failIfUncertain) { + throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: ' . $tzid); + } + + // If we got all the way here, we default to UTC. + return new \DateTimeZone(date_default_timezone_get()); + + } + +} diff --git a/3rdparty/VObject/VCardConverter.php b/3rdparty/VObject/VCardConverter.php new file mode 100644 index 000000000..86c097d51 --- /dev/null +++ b/3rdparty/VObject/VCardConverter.php @@ -0,0 +1,382 @@ +getDocumentType(); + if ($inputVersion===$targetVersion) { + return clone $input; + } + + if (!in_array($inputVersion, array(Document::VCARD21, Document::VCARD30, Document::VCARD40))) { + throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data'); + } + if (!in_array($targetVersion, array(Document::VCARD30, Document::VCARD40))) { + throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version'); + } + + $newVersion = $targetVersion===Document::VCARD40?'4.0':'3.0'; + + $output = new Component\VCard(array( + 'VERSION' => $newVersion, + )); + + foreach($input->children as $property) { + + $this->convertProperty($input, $output, $property, $targetVersion); + + } + + return $output; + + } + + /** + * Handles conversion of a single property. + * + * @param Component\VCard $input + * @param Component\VCard $output + * @param Property $property + * @param int $targetVersion + * @return void + */ + protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion) { + + // Skipping these, those are automatically added. + if (in_array($property->name, array('VERSION', 'PRODID'))) { + return; + } + + $parameters = $property->parameters(); + + $valueType = null; + if (isset($parameters['VALUE'])) { + $valueType = $parameters['VALUE']->getValue(); + unset($parameters['VALUE']); + } + if (!$valueType) { + $valueType = $property->getValueType(); + } + + $newProperty = null; + + if ($targetVersion===Document::VCARD30) { + + if ($property instanceof Property\Uri && in_array($property->name, array('PHOTO','LOGO','SOUND'))) { + + $newProperty = $this->convertUriToBinary($output, $property, $parameters); + + } elseif ($property->name === 'KIND') { + + switch(strtolower($property->getValue())) { + case 'org' : + // OS X addressbook property. + $newProperty = $output->createProperty('X-ABSHOWAS','COMPANY'); + break; + case 'individual' : + // Individual is implied, so we can just skip it. + return; + + case 'group' : + // OS X addressbook property + $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND','GROUP'); + break; + } + + + } + + } elseif ($targetVersion===Document::VCARD40) { + + // These properties were removed in vCard 4.0 + if (in_array($property->name, array('NAME', 'MAILER', 'LABEL', 'CLASS'))) { + return; + } + + if ($property instanceOf Property\Binary) { + + $newProperty = $this->convertBinaryToUri($output, $property, $parameters); + + } else { + switch($property->name) { + case 'X-ABSHOWAS' : + if (strtoupper($property->getValue()) === 'COMPANY') { + $newProperty = $output->createProperty('KIND','org'); + } + break; + case 'X-ADDRESSBOOKSERVER-KIND' : + if (strtoupper($property->getValue()) === 'GROUP') { + $newProperty = $output->createProperty('KIND','group'); + } + break; + } + + } + + } + + + if (is_null($newProperty)) { + + $newProperty = $output->createProperty( + $property->name, + $property->getParts(), + array(), // no parameters yet + $valueType + ); + + } + + // set property group + $newProperty->group = $property->group; + + if ($targetVersion===Document::VCARD40) { + $this->convertParameters40($newProperty, $parameters); + } else { + $this->convertParameters30($newProperty, $parameters); + } + + // Lastly, we need to see if there's a need for a VALUE parameter. + // + // We can do that by instantating a empty property with that name, and + // seeing if the default valueType is identical to the current one. + $tempProperty = $output->createProperty($newProperty->name); + if ($tempProperty->getValueType() !== $newProperty->getValueType()) { + $newProperty['VALUE'] = $newProperty->getValueType(); + } + + $output->add($newProperty); + + + } + + /** + * Converts a BINARY property to a URI property. + * + * vCard 4.0 no longer supports BINARY properties. + * + * @param Component\VCard $output + * @param Property\Uri $property The input property. + * @param $parameters List of parameters that will eventually be added to + * the new property. + * @return Property\Uri + */ + protected function convertBinaryToUri(Component\VCard $output, Property\Binary $property, array &$parameters) { + + $newProperty = $output->createProperty( + $property->name, + null, // no value + array(), // no parameters yet + 'URI' // Forcing the BINARY type + ); + + $mimeType = 'application/octet-stream'; + + // See if we can find a better mimetype. + if (isset($parameters['TYPE'])) { + + $newTypes = array(); + foreach($parameters['TYPE']->getParts() as $typePart) { + if (in_array( + strtoupper($typePart), + array('JPEG','PNG','GIF') + )) { + $mimeType = 'image/' . strtolower($typePart); + } else { + $newTypes[] = $typePart; + } + } + + // If there were any parameters we're not converting to a + // mime-type, we need to keep them. + if ($newTypes) { + $parameters['TYPE']->setParts($newTypes); + } else { + unset($parameters['TYPE']); + } + + } + + $newProperty->setValue('data:' . $mimeType . ';base64,' . base64_encode($property->getValue())); + + return $newProperty; + + } + + /** + * Converts a URI property to a BINARY property. + * + * In vCard 4.0 attachments are encoded as data: uri. Even though these may + * be valid in vCard 3.0 as well, we should convert those to BINARY if + * possible, to improve compatibility. + * + * @param Component\VCard $output + * @param Property\Uri $property The input property. + * @param $parameters List of parameters that will eventually be added to + * the new property. + * @return Property\Binary|null + */ + protected function convertUriToBinary(Component\VCard $output, Property\Uri $property, array &$parameters) { + + $value = $property->getValue(); + + // Only converting data: uris + if (substr($value, 0, 5)!=='data:') { + return; + } + + $newProperty = $output->createProperty( + $property->name, + null, // no value + array(), // no parameters yet + 'BINARY' + ); + + $mimeType = substr($value, 5, strpos($value, ',')-5); + if (strpos($mimeType, ';')) { + $mimeType = substr($mimeType,0,strpos($mimeType, ';')); + $newProperty->setValue(base64_decode(substr($value, strpos($value,',')+1))); + } else { + $newProperty->setValue(substr($value, strpos($value,',')+1)); + } + unset($value); + + $newProperty['ENCODING'] = 'b'; + switch($mimeType) { + + case 'image/jpeg' : + $newProperty['TYPE'] = 'JPEG'; + break; + case 'image/png' : + $newProperty['TYPE'] = 'PNG'; + break; + case 'image/gif' : + $newProperty['TYPE'] = 'GIF'; + break; + + } + + + return $newProperty; + + } + + /** + * Adds parameters to a new property for vCard 4.0 + * + * @param Property $newProperty + * @param array $parameters + * @return void + */ + protected function convertParameters40(Property $newProperty, array $parameters) { + + // Adding all parameters. + foreach($parameters as $param) { + + // vCard 2.1 allowed parameters with no name + if ($param->noName) $param->noName = false; + + switch($param->name) { + + // We need to see if there's any TYPE=PREF, because in vCard 4 + // that's now PREF=1. + case 'TYPE' : + foreach($param->getParts() as $paramPart) { + + if (strtoupper($paramPart)==='PREF') { + $newProperty->add('PREF','1'); + } else { + $newProperty->add($param->name, $paramPart); + } + + } + break; + // These no longer exist in vCard 4 + case 'ENCODING' : + case 'CHARSET' : + break; + + default : + $newProperty->add($param->name, $param->getParts()); + break; + + } + + } + + } + + /** + * Adds parameters to a new property for vCard 3.0 + * + * @param Property $newProperty + * @param array $parameters + * @return void + */ + protected function convertParameters30(Property $newProperty, array $parameters) { + + // Adding all parameters. + foreach($parameters as $param) { + + // vCard 2.1 allowed parameters with no name + if ($param->noName) $param->noName = false; + + switch($param->name) { + + case 'ENCODING' : + // This value only existed in vCard 2.1, and should be + // removed for anything else. + if (strtoupper($param->getValue())!=='QUOTED-PRINTABLE') { + $newProperty->add($param->name, $param->getParts()); + } + break; + + /* + * Converting PREF=1 to TYPE=PREF. + * + * Any other PREF numbers we'll drop. + */ + case 'PREF' : + if ($param->getValue()=='1') { + $newProperty->add('TYPE','PREF'); + } + break; + + default : + $newProperty->add($param->name, $param->getParts()); + break; + + } + + } + + } +} diff --git a/3rdparty/VObject/Version.php b/3rdparty/VObject/Version.php new file mode 100644 index 000000000..6fd6be40d --- /dev/null +++ b/3rdparty/VObject/Version.php @@ -0,0 +1,19 @@ +, usually */ +.fc-widget-content { /* , usually */ + border: 1px solid #ccc; + } + +.fc-state-highlight { /* today cell */ /* TODO: add .fc-today to */ + background: #ffc; + } + +.fc-cell-overlay { /* semi-transparent rectangle while dragging */ + background: #9cf; + opacity: .2; + filter: alpha(opacity=20); /* for IE */ + } + + + +/* Buttons +------------------------------------------------------------------------*/ + +.fc-button { + position: relative; + display: inline-block; + cursor: pointer; + } + +.fc-state-default { /* non-theme */ + border-style: solid; + border-width: 1px 0; + } + +.fc-button-inner { + position: relative; + float: left; + overflow: hidden; + } + +.fc-state-default .fc-button-inner { /* non-theme */ + border-style: solid; + border-width: 0 1px; + } + +.fc-button-content { + position: relative; + float: left; + height: 1.9em; + line-height: 1.9em; + padding: 0 .6em; + white-space: nowrap; + } + +/* icon (for jquery ui) */ + +.fc-button-content .fc-icon-wrap { + position: relative; + float: left; + top: 50%; + } + +.fc-button-content .ui-icon { + position: relative; + float: left; + margin-top: -50%; + *margin-top: 0; + *top: -50%; + } + +/* gloss effect */ + +.fc-state-default .fc-button-effect { + position: absolute; + top: 50%; + left: 0; + } + +.fc-state-default .fc-button-effect span { + position: absolute; + top: -100px; + left: 0; + width: 500px; + height: 100px; + border-width: 100px 0 0 1px; + border-style: solid; + border-color: #fff; + background: #444; + opacity: .09; + filter: alpha(opacity=9); + } + +/* button states (determines colors) */ + +.fc-state-default, +.fc-state-default .fc-button-inner { + border-style: solid; + border-color: #ccc #bbb #aaa; + background: #F3F3F3; + color: #000; + } + +.fc-state-hover, +.fc-state-hover .fc-button-inner { + border-color: #999; + } + +.fc-state-down, +.fc-state-down .fc-button-inner { + border-color: #555; + background: #777; + } + +.fc-state-active, +.fc-state-active .fc-button-inner { + border-color: #555; + background: #777; + color: #fff; + } + +.fc-state-disabled, +.fc-state-disabled .fc-button-inner { + color: #999; + border-color: #ddd; + } + +.fc-state-disabled { + cursor: default; + } + +.fc-state-disabled .fc-button-effect { + display: none; + } + + + +/* Global Event Styles +------------------------------------------------------------------------*/ + +.fc-event { + border-style: solid; + border-width: 0; + font-size: .85em; + cursor: default; + } + +a.fc-event, +.fc-event-draggable { + cursor: pointer; + } + +a.fc-event { + text-decoration: none; + } + +.fc-rtl .fc-event { + text-align: right; + } + +.fc-event-skin { + border-color: #36c; /* default BORDER color */ + background-color: #36c; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ + } + +.fc-event-inner { + position: relative; + width: 100%; + height: 100%; + border-style: solid; + border-width: 0; + overflow: hidden; + } + +.fc-event-time, +.fc-event-title { + padding: 0 1px; + } + +.fc .ui-resizable-handle { /*** TODO: don't use ui-resizable anymore, change class ***/ + display: block; + position: absolute; + z-index: 99999; + overflow: hidden; /* hacky spaces (IE6/7) */ + font-size: 300%; /* */ + line-height: 50%; /* */ + } + + + +/* Horizontal Events +------------------------------------------------------------------------*/ + +.fc-event-hori { + border-width: 1px 0; + margin-bottom: 1px; + } + +/* resizable */ + +.fc-event-hori .ui-resizable-e { + top: 0 !important; /* importants override pre jquery ui 1.7 styles */ + right: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: e-resize; + } + +.fc-event-hori .ui-resizable-w { + top: 0 !important; + left: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: w-resize; + } + +.fc-event-hori .ui-resizable-handle { + _padding-bottom: 14px; /* IE6 had 0 height */ + } + + + +/* Fake Rounded Corners (for buttons and events) +------------------------------------------------------------*/ + +.fc-corner-left { + margin-left: 1px; + } + +.fc-corner-left .fc-button-inner, +.fc-corner-left .fc-event-inner { + margin-left: -1px; + } + +.fc-corner-right { + margin-right: 1px; + } + +.fc-corner-right .fc-button-inner, +.fc-corner-right .fc-event-inner { + margin-right: -1px; + } + +.fc-corner-top { + margin-top: 1px; + } + +.fc-corner-top .fc-event-inner { + margin-top: -1px; + } + +.fc-corner-bottom { + margin-bottom: 1px; + } + +.fc-corner-bottom .fc-event-inner { + margin-bottom: -1px; + } + + + +/* Fake Rounded Corners SPECIFICALLY FOR EVENTS +-----------------------------------------------------------------*/ + +.fc-corner-left .fc-event-inner { + border-left-width: 1px; + } + +.fc-corner-right .fc-event-inner { + border-right-width: 1px; + } + +.fc-corner-top .fc-event-inner { + border-top-width: 1px; + } + +.fc-corner-bottom .fc-event-inner { + border-bottom-width: 1px; + } + + + +/* Reusable Separate-border Table +------------------------------------------------------------*/ + +table.fc-border-separate { + border-collapse: separate; + } + +.fc-border-separate th, +.fc-border-separate td { + border-width: 1px 0 0 1px; + } + +.fc-border-separate th.fc-last, +.fc-border-separate td.fc-last { + border-right-width: 1px; + } + +.fc-border-separate tr.fc-last th, +.fc-border-separate tr.fc-last td { + border-bottom-width: 1px; + } + +.fc-border-separate tbody tr.fc-first td, +.fc-border-separate tbody tr.fc-first th { + border-top-width: 0; + } + + + +/* Month View, Basic Week View, Basic Day View +------------------------------------------------------------------------*/ + +.fc-grid th { + text-align: center; + } + +.fc-grid .fc-day-number { + float: right; + padding: 0 2px; + } + +.fc-grid .fc-other-month .fc-day-number { + opacity: 0.3; + filter: alpha(opacity=30); /* for IE */ + /* opacity with small font can sometimes look too faded + might want to set the 'color' property instead + making day-numbers bold also fixes the problem */ + } + +.fc-grid .fc-day-content { + clear: both; + padding: 2px 2px 1px; /* distance between events and day edges */ + } + +/* event styles */ + +.fc-grid .fc-event-time { + font-weight: bold; + } + +/* right-to-left */ + +.fc-rtl .fc-grid .fc-day-number { + float: left; + } + +.fc-rtl .fc-grid .fc-event-time { + float: right; + } + + + +/* Agenda Week View, Agenda Day View +------------------------------------------------------------------------*/ + +.fc-agenda table { + border-collapse: separate; + } + +.fc-agenda-days th { + text-align: center; + } + +.fc-agenda .fc-agenda-axis { + width: 50px; + padding: 0 4px; + vertical-align: middle; + text-align: right; + white-space: nowrap; + font-weight: normal; + } + +.fc-agenda .fc-day-content { + padding: 2px 2px 1px; + } + +/* make axis border take precedence */ + +.fc-agenda-days .fc-agenda-axis { + border-right-width: 1px; + } + +.fc-agenda-days .fc-col0 { + border-left-width: 0; + } + +/* all-day area */ + +.fc-agenda-allday th { + border-width: 0 1px; + } + +.fc-agenda-allday .fc-day-content { + min-height: 34px; /* TODO: doesnt work well in quirksmode */ + _height: 34px; + } + +/* divider (between all-day and slots) */ + +.fc-agenda-divider-inner { + height: 2px; + overflow: hidden; + } + +.fc-widget-header .fc-agenda-divider-inner { + background: #eee; + } + +/* slot rows */ + +.fc-agenda-slots th { + border-width: 1px 1px 0; + } + +.fc-agenda-slots td { + border-width: 1px 0 0; + background: none; + } + +.fc-agenda-slots td div { + height: 20px; + } + +.fc-agenda-slots tr.fc-slot0 th, +.fc-agenda-slots tr.fc-slot0 td { + border-top-width: 0; + } + +.fc-agenda-slots tr.fc-minor th, +.fc-agenda-slots tr.fc-minor td { + border-top-style: dotted; + } + +.fc-agenda-slots tr.fc-minor th.ui-widget-header { + *border-top-style: solid; /* doesn't work with background in IE6/7 */ + } + + + +/* Vertical Events +------------------------------------------------------------------------*/ + +.fc-event-vert { + border-width: 0 1px; + } + +.fc-event-vert .fc-event-head, +.fc-event-vert .fc-event-content { + position: relative; + z-index: 2; + width: 100%; + overflow: hidden; + } + +.fc-event-vert .fc-event-time { + white-space: nowrap; + font-size: 10px; + } + +.fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + opacity: .3; + filter: alpha(opacity=30); + } + +.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */ +.fc-select-helper .fc-event-bg { + display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */ + } + +/* resizable */ + +.fc-event-vert .ui-resizable-s { + bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */ + width: 100% !important; + height: 8px !important; + overflow: hidden !important; + line-height: 8px !important; + font-size: 11px !important; + font-family: monospace; + text-align: center; + cursor: s-resize; + } + +.fc-agenda .ui-resizable-resizing { /* TODO: better selector */ + _overflow: hidden; + } + + diff --git a/3rdparty/css/fullcalendar.print.css b/3rdparty/css/fullcalendar.print.css new file mode 100644 index 000000000..227b80e0b --- /dev/null +++ b/3rdparty/css/fullcalendar.print.css @@ -0,0 +1,61 @@ +/* + * FullCalendar v1.5.4 Print Stylesheet + * + * Include this stylesheet on your page to get a more printer-friendly calendar. + * When including this stylesheet, use the media='print' attribute of the tag. + * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css. + * + * Copyright (c) 2011 Adam Shaw + * Dual licensed under the MIT and GPL licenses, located in + * MIT-LICENSE.txt and GPL-LICENSE.txt respectively. + * + * Date: Tue Sep 4 23:38:33 2012 -0700 + * + */ + + + /* Events +-----------------------------------------------------*/ + +.fc-event-skin { + background: none !important; + color: #000 !important; + } + +/* horizontal events */ + +.fc-event-hori { + border-width: 0 0 1px 0 !important; + border-bottom-style: dotted !important; + border-bottom-color: #000 !important; + padding: 1px 0 0 0 !important; + } + +.fc-event-hori .fc-event-inner { + border-width: 0 !important; + padding: 0 1px !important; + } + +/* vertical events */ + +.fc-event-vert { + border-width: 0 0 0 1px !important; + border-left-style: dotted !important; + border-left-color: #000 !important; + padding: 0 1px 0 0 !important; + } + +.fc-event-vert .fc-event-inner { + border-width: 0 !important; + padding: 1px 0 !important; + } + +.fc-event-bg { + display: none !important; + } + +.fc-event .ui-resizable-handle { + display: none !important; + } + + diff --git a/3rdparty/css/timepicker.css b/3rdparty/css/timepicker.css new file mode 100644 index 000000000..b4545376d --- /dev/null +++ b/3rdparty/css/timepicker.css @@ -0,0 +1,69 @@ +/* + * Timepicker stylesheet + * Highly inspired from datepicker + * FG - Nov 2010 - Web3R + * + * version 0.0.3 : Fixed some settings, more dynamic + * version 0.0.4 : Removed width:100% on tables + * version 0.1.1 : set width 0 on tables to fix an ie6 bug + */ + +.ui-timepicker-inline { display: inline; } + +#ui-timepicker-div { padding: 0.2em; background-color: #fff; } +.ui-timepicker-table { display: inline-table; width: 0; } +.ui-timepicker-table table { margin:0.15em 0 0 0; border-collapse: collapse; } + +.ui-timepicker-hours, .ui-timepicker-minutes { padding: 0.2em; } + +.ui-timepicker-table .ui-timepicker-title { line-height: 1.8em; text-align: center; } +.ui-timepicker-table td { padding: 0.1em; width: 2.2em; } +.ui-timepicker-table th.periods { padding: 0.1em; width: 2.2em; } + +/* span for disabled cells */ +.ui-timepicker-table td span { + display:block; + padding:0.2em 0.3em 0.2em 0.5em; + width: 1.2em; + + text-align:right; + text-decoration:none; +} +/* anchors for clickable cells */ +.ui-timepicker-table td a { + display:block; + padding:0.2em 0.3em 0.2em 0.5em; + width: 1.2em; + cursor: pointer; + text-align:right; + text-decoration:none; +} + + +/* buttons and button pane styling */ +.ui-timepicker .ui-timepicker-buttonpane { + background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; +} +.ui-timepicker .ui-timepicker-buttonpane button { margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } +/* The close button */ +.ui-timepicker .ui-timepicker-close { float: right } + +/* the now button */ +.ui-timepicker .ui-timepicker-now { float: left; } + +/* the deselect button */ +.ui-timepicker .ui-timepicker-deselect { float: left; } + + +/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ +.ui-timepicker-cover { + display: none; /*sorry for IE5*/ + display/**/: block; /*sorry for IE5*/ + position: absolute; /*must have*/ + z-index: -1; /*must have*/ + filter: mask(); /*must have*/ + top: -4px; /*must have*/ + left: -4px; /*must have*/ + width: 200px; /*must have*/ + height: 200px; /*must have*/ +} \ No newline at end of file diff --git a/3rdparty/css/tipsy.css b/3rdparty/css/tipsy.css new file mode 100644 index 000000000..f170fb710 --- /dev/null +++ b/3rdparty/css/tipsy.css @@ -0,0 +1,25 @@ +.tipsy { font-size: 10px; position: absolute; padding: 5px; z-index: 100000; } + .tipsy-inner { background-color: #000; color: #FFF; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; } + + /* Rounded corners */ + .tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; } + + /* Uncomment for shadow */ + /*.tipsy-inner { box-shadow: 0 0 5px #000000; -webkit-box-shadow: 0 0 5px #000000; -moz-box-shadow: 0 0 5px #000000; }*/ + + .tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #000; } + + /* Rules to colour arrows */ + .tipsy-arrow-n { border-bottom-color: #000; } + .tipsy-arrow-s { border-top-color: #000; } + .tipsy-arrow-e { border-left-color: #000; } + .tipsy-arrow-w { border-right-color: #000; } + + .tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} + .tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} + .tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; } + .tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; } diff --git a/3rdparty/css/tipsy.mod.css b/3rdparty/css/tipsy.mod.css new file mode 100644 index 000000000..62f4c34c8 --- /dev/null +++ b/3rdparty/css/tipsy.mod.css @@ -0,0 +1,25 @@ +.tipsy { font-size: 10px; position: absolute; padding: 5px; z-index: 100000; } + .tipsy-inner { background-color: #f0f0f0; color: #222222; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; } + + /* Rounded corners */ + .tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; } + + /* Uncomment for shadow */ + /*.tipsy-inner { box-shadow: 0 0 5px #000000; -webkit-box-shadow: 0 0 5px #000000; -moz-box-shadow: 0 0 5px #000000; }*/ + + .tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #f0f0f0; } + + /* Rules to colour arrows */ + .tipsy-arrow-n { border-bottom-color: #f0f0f0; } + .tipsy-arrow-s { border-top-color: #f0f0f0; } + .tipsy-arrow-e { border-left-color: #f0f0f0; } + .tipsy-arrow-w { border-right-color: #f0f0f0; } + + .tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} + .tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} + .tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; } + .tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; } diff --git a/3rdparty/js/backbone/MIT License b/3rdparty/js/backbone/MIT License new file mode 100644 index 000000000..0ed873491 --- /dev/null +++ b/3rdparty/js/backbone/MIT License @@ -0,0 +1,22 @@ +Copyright (c) 2010-2013 Jeremy Ashkenas, DocumentCloud + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/3rdparty/js/backbone/backbone.js b/3rdparty/js/backbone/backbone.js new file mode 100644 index 000000000..9682be585 --- /dev/null +++ b/3rdparty/js/backbone/backbone.js @@ -0,0 +1,1498 @@ +// Backbone.js 0.9.10 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(){ + + // Initial Setup + // ------------- + + // Save a reference to the global object (`window` in the browser, `exports` + // on the server). + var root = this; + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create a local reference to array methods. + var array = []; + var push = array.push; + var slice = array.slice; + var splice = array.splice; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both CommonJS and the browser. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = root.Backbone = {}; + } + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '0.9.10'; + + // Require Underscore, if we're on the server, and it's not already present. + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + + // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. + Backbone.$ = root.jQuery || root.Zepto || root.ender; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // --------------- + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Implement fancy features of the Events API such as multiple event + // names `"change blur"` and jQuery-style event maps `{change: action}` + // in terms of the existing API. + var eventsApi = function(obj, action, name, rest) { + if (!name) return true; + if (typeof name === 'object') { + for (var key in name) { + obj[action].apply(obj, [key, name[key]].concat(rest)); + } + } else if (eventSplitter.test(name)) { + var names = name.split(eventSplitter); + for (var i = 0, l = names.length; i < l; i++) { + obj[action].apply(obj, [names[i]].concat(rest)); + } + } else { + return true; + } + }; + + // Optimized internal dispatch function for triggering events. Tries to + // keep the usual cases speedy (most Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); + return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]); + return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]); + return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]); + return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + } + }; + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback + // functions to an event; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + + // Bind one or more space separated events, or an events map, + // to a `callback` function. Passing `"all"` will bind the callback to + // all events fired. + on: function(name, callback, context) { + if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this; + this._events || (this._events = {}); + var list = this._events[name] || (this._events[name] = []); + list.push({callback: callback, context: context, ctx: context || this}); + return this; + }, + + // Bind events to only be triggered a single time. After the first time + // the callback is invoked, it will be removed. + once: function(name, callback, context) { + if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this; + var self = this; + var once = _.once(function() { + self.off(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + this.on(name, once, context); + return this; + }, + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + off: function(name, callback, context) { + var list, ev, events, names, i, l, j, k; + if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; + if (!name && !callback && !context) { + this._events = {}; + return this; + } + + names = name ? [name] : _.keys(this._events); + for (i = 0, l = names.length; i < l; i++) { + name = names[i]; + if (list = this._events[name]) { + events = []; + if (callback || context) { + for (j = 0, k = list.length; j < k; j++) { + ev = list[j]; + if ((callback && callback !== ev.callback && + callback !== ev.callback._callback) || + (context && context !== ev.context)) { + events.push(ev); + } + } + } + this._events[name] = events; + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(name) { + if (!this._events) return this; + var args = slice.call(arguments, 1); + if (!eventsApi(this, 'trigger', name, args)) return this; + var events = this._events[name]; + var allEvents = this._events.all; + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, arguments); + return this; + }, + + // An inversion-of-control version of `on`. Tell *this* object to listen to + // an event in another object ... keeping track of what it's listening to. + listenTo: function(obj, name, callback) { + var listeners = this._listeners || (this._listeners = {}); + var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); + listeners[id] = obj; + obj.on(name, typeof name === 'object' ? this : callback, this); + return this; + }, + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening: function(obj, name, callback) { + var listeners = this._listeners; + if (!listeners) return; + if (obj) { + obj.off(name, typeof name === 'object' ? this : callback, this); + if (!name && !callback) delete listeners[obj._listenerId]; + } else { + if (typeof name === 'object') callback = this; + for (var id in listeners) { + listeners[id].off(name, callback, this); + } + this._listeners = {}; + } + return this; + } + }; + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Create a new model, with defined attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var defaults; + var attrs = attributes || {}; + this.cid = _.uniqueId('c'); + this.attributes = {}; + if (options && options.collection) this.collection = options.collection; + if (options && options.parse) attrs = this.parse(attrs, options) || {}; + if (defaults = _.result(this, 'defaults')) { + attrs = _.defaults({}, attrs, defaults); + } + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // ---------------------------------------------------------------------- + + // Set a hash of model attributes on the object, firing `"change"` unless + // you choose to silence it. + set: function(key, val, options) { + var attr, attrs, unset, changes, silent, changing, prev, current; + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + this.changed[attr] = val; + } else { + delete this.changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + if (changing) return this; + if (!silent) { + while (this._pending) { + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"` unless you choose + // to silence it. `unset` is a noop if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"` unless you choose + // to silence it. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false; + var old = this._changing ? this._previousAttributes : this.attributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // --------------------------------------------------------------------- + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overriden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var success = options.success; + options.success = function(model, resp, options) { + if (!model.set(model.parse(resp, options), options)) return false; + if (success) success(model, resp, options); + }; + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + var attrs, success, method, xhr, attributes = this.attributes; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. + if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; + + options = _.extend({validate: true}, options); + + // Do not persist invalid models. + if (!this._validate(attrs, options)) return false; + + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + if (options.parse === void 0) options.parse = true; + success = options.success; + options.success = function(model, resp, options) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = model.parse(resp, options); + if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } + if (success) success(model, resp, options); + }; + + // Finish configuring and sending the Ajax request. + method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch') options.attrs = attrs; + xhr = this.sync(method, this, options); + + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var destroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(model, resp, options) { + if (options.wait || model.isNew()) destroy(); + if (success) success(model, resp, options); + }; + + if (this.isNew()) { + options.success(this, null, options); + return false; + } + + var xhr = this.sync('delete', this, options); + if (!options.wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return !this.validate || !this.validate(this.attributes, options); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire a general + // `"error"` event and call the error callback, if specified. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, options || {}); + return false; + } + + }); + + // Backbone.Collection + // ------------------- + + // Provides a standard collection class for our sets of models, ordered + // or unordered. If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this.models = []; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. + add: function(models, options) { + models = _.isArray(models) ? models.slice() : [models]; + options || (options = {}); + var i, l, model, attrs, existing, doSort, add, at, sort, sortAttr; + add = []; + at = options.at; + sort = this.comparator && (at == null) && options.sort != false; + sortAttr = _.isString(this.comparator) ? this.comparator : null; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + for (i = 0, l = models.length; i < l; i++) { + if (!(model = this._prepareModel(attrs = models[i], options))) { + this.trigger('invalid', this, attrs, options); + continue; + } + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + if (existing = this.get(model)) { + if (options.merge) { + existing.set(attrs === model ? model.attributes : attrs, options); + if (sort && !doSort && existing.hasChanged(sortAttr)) doSort = true; + } + continue; + } + + // This is a new model, push it to the `add` list. + add.push(model); + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + model.on('all', this._onModelEvent, this); + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + + // See if sorting is needed, update `length` and splice in new models. + if (add.length) { + if (sort) doSort = true; + this.length += add.length; + if (at != null) { + splice.apply(this.models, [at, 0].concat(add)); + } else { + push.apply(this.models, add); + } + } + + // Silently sort the collection if appropriate. + if (doSort) this.sort({silent: true}); + + if (options.silent) return this; + + // Trigger `add` events. + for (i = 0, l = add.length; i < l; i++) { + (model = add[i]).trigger('add', model, this, options); + } + + // Trigger `sort` if the collection was sorted. + if (doSort) this.trigger('sort', this, options); + + return this; + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + models = _.isArray(models) ? models.slice() : [models]; + options || (options = {}); + var i, l, index, model; + for (i = 0, l = models.length; i < l; i++) { + model = this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byId[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return this; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: this.length}, options)); + return model; + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: 0}, options)); + return model; + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Slice out a sub-array of models from the collection. + slice: function(begin, end) { + return this.models.slice(begin, end); + }, + + // Get a model from the set by id. + get: function(obj) { + if (obj == null) return void 0; + this._idAttr || (this._idAttr = this.model.prototype.idAttribute); + return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of `filter`. + where: function(attrs) { + if (_.isEmpty(attrs)) return []; + return this.filter(function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + if (!this.comparator) { + throw new Error('Cannot sort a set without a comparator'); + } + options || (options = {}); + + // Run sort based on type of `comparator`. + if (_.isString(this.comparator) || this.comparator.length === 1) { + this.models = this.sortBy(this.comparator, this); + } else { + this.models.sort(_.bind(this.comparator, this)); + } + + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.invoke(this.models, 'get', attr); + }, + + // Smartly update a collection with a change set of models, adding, + // removing, and merging as necessary. + update: function(models, options) { + options = _.extend({add: true, merge: true, remove: true}, options); + if (options.parse) models = this.parse(models, options); + var model, i, l, existing; + var add = [], remove = [], modelMap = {}; + + // Allow a single model (or no argument) to be passed. + if (!_.isArray(models)) models = models ? [models] : []; + + // Proxy to `add` for this case, no need to iterate... + if (options.add && !options.remove) return this.add(models, options); + + // Determine which models to add and merge, and which to remove. + for (i = 0, l = models.length; i < l; i++) { + model = models[i]; + existing = this.get(model); + if (options.remove && existing) modelMap[existing.cid] = true; + if ((options.add && !existing) || (options.merge && existing)) { + add.push(model); + } + } + if (options.remove) { + for (i = 0, l = this.models.length; i < l; i++) { + model = this.models[i]; + if (!modelMap[model.cid]) remove.push(model); + } + } + + // Remove models (if applicable) before we add and merge the rest. + if (remove.length) this.remove(remove, options); + if (add.length) this.add(add, options); + return this; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any `add` or `remove` events. Fires `reset` when finished. + reset: function(models, options) { + options || (options = {}); + if (options.parse) models = this.parse(models, options); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + options.previousModels = this.models.slice(); + this._reset(); + if (models) this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `update: true` is passed, the response + // data will be passed through the `update` method instead of `reset`. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var success = options.success; + options.success = function(collection, resp, options) { + var method = options.update ? 'update' : 'reset'; + collection[method](resp, options); + if (success) success(collection, resp, options); + }; + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + if (!(model = this._prepareModel(model, options))) return false; + if (!options.wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(model, resp, options) { + if (options.wait) collection.add(model, options); + if (success) success(model, resp, options); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models); + }, + + // Reset all internal state. Called when the collection is reset. + _reset: function() { + this.length = 0; + this.models.length = 0; + this._byId = {}; + }, + + // Prepare a model or hash of attributes to be added to this collection. + _prepareModel: function(attrs, options) { + if (attrs instanceof Model) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options || (options = {}); + options.collection = this; + var model = new this.model(attrs, options); + if (!model._validate(attrs, options)) return false; + return model; + }, + + // Internal method to remove a model's ties to a collection. + _removeReference: function(model) { + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + if (model.id != null) this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + }, + + sortedIndex: function (model, value, context) { + value || (value = this.comparator); + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _.sortedIndex(this.models, model, iterator, context); + } + + }); + + // Underscore methods that we want to implement on the Collection. + var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', + 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', + 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', + 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', + 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', + 'isEmpty', 'chain']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.models); + return _[method].apply(_, args); + }; + }); + + // Underscore methods that take a property name as an argument. + var attributeMethods = ['groupBy', 'countBy', 'sortBy']; + + // Use attributes instead of properties. + _.each(attributeMethods, function(method) { + Collection.prototype[method] = function(value, context) { + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _[method](this.models, iterator, context); + }; + }); + + // Backbone.Router + // --------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (!callback) callback = this[name]; + Backbone.history.route(route, _.bind(function(fragment) { + var args = this._extractParameters(route, fragment); + callback && callback.apply(this, args); + this.trigger.apply(this, ['route:' + name].concat(args)); + this.trigger('route', name, args); + Backbone.history.trigger('route', this, name, args); + }, this)); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional){ + return optional ? match : '([^\/]+)'; + }) + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted parameters. + _extractParameters: function(route, fragment) { + return route.exec(fragment).slice(1); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on URL fragments. If the + // browser does not support `onhashchange`, falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Cached regex for removing a trailing slash. + var trailingSlash = /\/$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || !this._wantsHashChange || forcePushState) { + fragment = this.location.pathname; + var root = this.root.replace(trailingSlash, ''); + if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, {root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + if (oldIE && this._wantsHashChange) { + this.iframe = Backbone.$('' : ''); + + return html; + }, + + /* Special function that update the minutes selection in currently visible timepicker + * called on hour selection when onMinuteShow is defined */ + _updateMinuteDisplay: function (inst) { + var newHtml = this._generateHTMLMinutes(inst); + inst.tpDiv.find('td.ui-timepicker-minutes').html(newHtml); + this._rebindDialogEvents(inst); + // after the picker html is appended bind the click & double click events (faster in IE this way + // then letting the browser interpret the inline events) + // yes I know, duplicate code, sorry +/* .find('.ui-timepicker-minute-cell') + .bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectMinutes, this)) + .bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectMinutes, this)); +*/ + + }, + + /* + * Generate the minutes table + * This is separated from the _generateHTML function because is can be called separately (when hours changes) + */ + _generateHTMLMinutes: function (inst) { + + var m, row, html = '', + rows = this._get(inst, 'rows'), + minutes = Array(), + minutes_options = this._get(inst, 'minutes'), + minutesPerRow = null, + minuteCounter = 0, + showMinutesLeadingZero = (this._get(inst, 'showMinutesLeadingZero') == true), + onMinuteShow = this._get(inst, 'onMinuteShow'), + minuteLabel = this._get(inst, 'minuteText'); + + if ( ! minutes_options.starts) { + minutes_options.starts = 0; + } + if ( ! minutes_options.ends) { + minutes_options.ends = 59; + } + for (m = minutes_options.starts; m <= minutes_options.ends; m += minutes_options.interval) { + minutes.push(m); + } + minutesPerRow = Math.round(minutes.length / rows + 0.49); // always round up + + /* + * The minutes table + */ + // if currently selected minute is not enabled, we have a problem and need to select a new minute. + if (onMinuteShow && + (onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours , inst.minutes]) == false) ) { + // loop minutes and select first available + for (minuteCounter = 0; minuteCounter < minutes.length; minuteCounter += 1) { + m = minutes[minuteCounter]; + if (onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours, m])) { + inst.minutes = m; + break; + } + } + } + + + + html += '
' + + minuteLabel + + '
' + + ''; + + minuteCounter = 0; + for (row = 1; row <= rows; row++) { + html += ''; + while (minuteCounter < row * minutesPerRow) { + var m = minutes[minuteCounter]; + var displayText = ''; + if (m !== undefined ) { + displayText = (m < 10) && showMinutesLeadingZero ? "0" + m.toString() : m.toString(); + } + html += this._generateHTMLMinuteCell(inst, m, displayText); + minuteCounter++; + } + html += ''; + } + + html += '
'; + + return html; + }, + + /* Generate the content of a "Hour" cell */ + _generateHTMLHourCell: function (inst, hour, showPeriod, showLeadingZero) { + + var displayHour = hour; + if ((hour > 12) && showPeriod) { + displayHour = hour - 12; + } + if ((displayHour == 0) && showPeriod) { + displayHour = 12; + } + if ((displayHour < 10) && showLeadingZero) { + displayHour = '0' + displayHour; + } + + var html = ""; + var enabled = true; + var onHourShow = this._get(inst, 'onHourShow'); //custom callback + + if (hour == undefined) { + html = ' '; + return html; + } + + if (onHourShow) { + enabled = onHourShow.apply((inst.input ? inst.input[0] : null), [hour]); + } + + if (enabled) { + html = '' + + '' + + displayHour.toString() + + ''; + } + else { + html = + '' + + '' + + displayHour.toString() + + '' + + ''; + } + return html; + }, + + /* Generate the content of a "Hour" cell */ + _generateHTMLMinuteCell: function (inst, minute, displayText) { + var html = ""; + var enabled = true; + var onMinuteShow = this._get(inst, 'onMinuteShow'); //custom callback + if (onMinuteShow) { + //NEW: 2011-02-03 we should give the hour as a parameter as well! + enabled = onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours,minute]); //trigger callback + } + + if (minute == undefined) { + html = ' '; + return html; + } + + if (enabled) { + html = '' + + '' + + displayText + + ''; + } + else { + + html = '' + + '' + + displayText + + '' + + ''; + } + return html; + }, + + + /* Detach a timepicker from its control. + @param target element - the target input field or division or span */ + _destroyTimepicker: function(target) { + var $target = $(target); + var inst = $.data(target, PROP_NAME); + if (!$target.hasClass(this.markerClassName)) { + return; + } + var nodeName = target.nodeName.toLowerCase(); + $.removeData(target, PROP_NAME); + if (nodeName == 'input') { + inst.append.remove(); + inst.trigger.remove(); + $target.removeClass(this.markerClassName) + .unbind('focus.timepicker', this._showTimepicker) + .unbind('click.timepicker', this._adjustZIndex); + } else if (nodeName == 'div' || nodeName == 'span') + $target.removeClass(this.markerClassName).empty(); + }, + + /* Enable the date picker to a jQuery selection. + @param target element - the target input field or division or span */ + _enableTimepicker: function(target) { + var $target = $(target), + target_id = $target.attr('id'), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + var nodeName = target.nodeName.toLowerCase(); + if (nodeName == 'input') { + target.disabled = false; + var button = this._get(inst, 'button'); + $(button).removeClass('ui-state-disabled').disabled = false; + inst.trigger.filter('button'). + each(function() { this.disabled = false; }).end(); + } + else if (nodeName == 'div' || nodeName == 'span') { + var inline = $target.children('.' + this._inlineClass); + inline.children().removeClass('ui-state-disabled'); + inline.find('button').each( + function() { this.disabled = false } + ) + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value == target_id ? null : value); }); // delete entry + }, + + /* Disable the time picker to a jQuery selection. + @param target element - the target input field or division or span */ + _disableTimepicker: function(target) { + var $target = $(target); + var inst = $.data(target, PROP_NAME); + if (!$target.hasClass(this.markerClassName)) { + return; + } + var nodeName = target.nodeName.toLowerCase(); + if (nodeName == 'input') { + var button = this._get(inst, 'button'); + + $(button).addClass('ui-state-disabled').disabled = true; + target.disabled = true; + + inst.trigger.filter('button'). + each(function() { this.disabled = true; }).end(); + + } + else if (nodeName == 'div' || nodeName == 'span') { + var inline = $target.children('.' + this._inlineClass); + inline.children().addClass('ui-state-disabled'); + inline.find('button').each( + function() { this.disabled = true } + ) + + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value == target ? null : value); }); // delete entry + this._disabledInputs[this._disabledInputs.length] = $target.attr('id'); + }, + + /* Is the first field in a jQuery collection disabled as a timepicker? + @param target_id element - the target input field or division or span + @return boolean - true if disabled, false if enabled */ + _isDisabledTimepicker: function (target_id) { + if ( ! target_id) { return false; } + for (var i = 0; i < this._disabledInputs.length; i++) { + if (this._disabledInputs[i] == target_id) { return true; } + } + return false; + }, + + /* Check positioning to remain on screen. */ + _checkOffset: function (inst, offset, isFixed) { + var tpWidth = inst.tpDiv.outerWidth(); + var tpHeight = inst.tpDiv.outerHeight(); + var inputWidth = inst.input ? inst.input.outerWidth() : 0; + var inputHeight = inst.input ? inst.input.outerHeight() : 0; + var viewWidth = document.documentElement.clientWidth + $(document).scrollLeft(); + var viewHeight = document.documentElement.clientHeight + $(document).scrollTop(); + + offset.left -= (this._get(inst, 'isRTL') ? (tpWidth - inputWidth) : 0); + offset.left -= (isFixed && offset.left == inst.input.offset().left) ? $(document).scrollLeft() : 0; + offset.top -= (isFixed && offset.top == (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; + + // now check if datepicker is showing outside window viewport - move to a better place if so. + offset.left -= Math.min(offset.left, (offset.left + tpWidth > viewWidth && viewWidth > tpWidth) ? + Math.abs(offset.left + tpWidth - viewWidth) : 0); + offset.top -= Math.min(offset.top, (offset.top + tpHeight > viewHeight && viewHeight > tpHeight) ? + Math.abs(tpHeight + inputHeight) : 0); + + return offset; + }, + + /* Find an object's position on the screen. */ + _findPos: function (obj) { + var inst = this._getInst(obj); + var isRTL = this._get(inst, 'isRTL'); + while (obj && (obj.type == 'hidden' || obj.nodeType != 1)) { + obj = obj[isRTL ? 'previousSibling' : 'nextSibling']; + } + var position = $(obj).offset(); + return [position.left, position.top]; + }, + + /* Retrieve the size of left and top borders for an element. + @param elem (jQuery object) the element of interest + @return (number[2]) the left and top borders */ + _getBorders: function (elem) { + var convert = function (value) { + return { thin: 1, medium: 2, thick: 3}[value] || value; + }; + return [parseFloat(convert(elem.css('border-left-width'))), + parseFloat(convert(elem.css('border-top-width')))]; + }, + + + /* Close time picker if clicked elsewhere. */ + _checkExternalClick: function (event) { + if (!$.timepicker._curInst) { return; } + var $target = $(event.target); + if ($target[0].id != $.timepicker._mainDivId && + $target.parents('#' + $.timepicker._mainDivId).length == 0 && + !$target.hasClass($.timepicker.markerClassName) && + !$target.hasClass($.timepicker._triggerClass) && + $.timepicker._timepickerShowing && !($.timepicker._inDialog && $.blockUI)) + $.timepicker._hideTimepicker(); + }, + + /* Hide the time picker from view. + @param input element - the input field attached to the time picker */ + _hideTimepicker: function (input) { + var inst = this._curInst; + if (!inst || (input && inst != $.data(input, PROP_NAME))) { return; } + if (this._timepickerShowing) { + var showAnim = this._get(inst, 'showAnim'); + var duration = this._get(inst, 'duration'); + var postProcess = function () { + $.timepicker._tidyDialog(inst); + this._curInst = null; + }; + if ($.effects && $.effects[showAnim]) { + inst.tpDiv.hide(showAnim, $.timepicker._get(inst, 'showOptions'), duration, postProcess); + } + else { + inst.tpDiv[(showAnim == 'slideDown' ? 'slideUp' : + (showAnim == 'fadeIn' ? 'fadeOut' : 'hide'))]((showAnim ? duration : null), postProcess); + } + if (!showAnim) { postProcess(); } + + this._timepickerShowing = false; + + this._lastInput = null; + if (this._inDialog) { + this._dialogInput.css({ position: 'absolute', left: '0', top: '-100px' }); + if ($.blockUI) { + $.unblockUI(); + $('body').append(this.tpDiv); + } + } + this._inDialog = false; + + var onClose = this._get(inst, 'onClose'); + if (onClose) { + onClose.apply( + (inst.input ? inst.input[0] : null), + [(inst.input ? inst.input.val() : ''), inst]); // trigger custom callback + } + + } + }, + + + + /* Tidy up after a dialog display. */ + _tidyDialog: function (inst) { + inst.tpDiv.removeClass(this._dialogClass).unbind('.ui-timepicker'); + }, + + /* Retrieve the instance data for the target control. + @param target element - the target input field or division or span + @return object - the associated instance data + @throws error if a jQuery problem getting data */ + _getInst: function (target) { + try { + return $.data(target, PROP_NAME); + } + catch (err) { + throw 'Missing instance data for this timepicker'; + } + }, + + /* Get a setting value, defaulting if necessary. */ + _get: function (inst, name) { + return inst.settings[name] !== undefined ? + inst.settings[name] : this._defaults[name]; + }, + + /* Parse existing time and initialise time picker. */ + _setTimeFromField: function (inst) { + if (inst.input.val() == inst.lastVal) { return; } + var defaultTime = this._get(inst, 'defaultTime'); + + var timeToParse = defaultTime == 'now' ? this._getCurrentTimeRounded(inst) : defaultTime; + if ((inst.inline == false) && (inst.input.val() != '')) { timeToParse = inst.input.val() } + + if (timeToParse instanceof Date) { + inst.hours = timeToParse.getHours(); + inst.minutes = timeToParse.getMinutes(); + } else { + var timeVal = inst.lastVal = timeToParse; + if (timeToParse == '') { + inst.hours = -1; + inst.minutes = -1; + } else { + var time = this.parseTime(inst, timeVal); + inst.hours = time.hours; + inst.minutes = time.minutes; + } + } + + + $.timepicker._updateTimepicker(inst); + }, + + /* Update or retrieve the settings for an existing time picker. + @param target element - the target input field or division or span + @param name object - the new settings to update or + string - the name of the setting to change or retrieve, + when retrieving also 'all' for all instance settings or + 'defaults' for all global defaults + @param value any - the new value for the setting + (omit if above is an object or to retrieve a value) */ + _optionTimepicker: function(target, name, value) { + var inst = this._getInst(target); + if (arguments.length == 2 && typeof name == 'string') { + return (name == 'defaults' ? $.extend({}, $.timepicker._defaults) : + (inst ? (name == 'all' ? $.extend({}, inst.settings) : + this._get(inst, name)) : null)); + } + var settings = name || {}; + if (typeof name == 'string') { + settings = {}; + settings[name] = value; + } + if (inst) { + if (this._curInst == inst) { + this._hideTimepicker(); + } + extendRemove(inst.settings, settings); + this._updateTimepicker(inst); + } + }, + + + /* Set the time for a jQuery selection. + @param target element - the target input field or division or span + @param time String - the new time */ + _setTimeTimepicker: function(target, time) { + var inst = this._getInst(target); + if (inst) { + this._setTime(inst, time); + this._updateTimepicker(inst); + this._updateAlternate(inst, time); + } + }, + + /* Set the time directly. */ + _setTime: function(inst, time, noChange) { + var origHours = inst.hours; + var origMinutes = inst.minutes; + var time = this.parseTime(inst, time); + inst.hours = time.hours; + inst.minutes = time.minutes; + + if ((origHours != inst.hours || origMinutes != inst.minuts) && !noChange) { + inst.input.trigger('change'); + } + this._updateTimepicker(inst); + this._updateSelectedValue(inst); + }, + + /* Return the current time, ready to be parsed, rounded to the closest 5 minute */ + _getCurrentTimeRounded: function (inst) { + var currentTime = new Date(), + currentMinutes = currentTime.getMinutes(), + // round to closest 5 + adjustedMinutes = Math.round( currentMinutes / 5 ) * 5; + currentTime.setMinutes(adjustedMinutes); + return currentTime; + }, + + /* + * Parse a time string into hours and minutes + */ + parseTime: function (inst, timeVal) { + var retVal = new Object(); + retVal.hours = -1; + retVal.minutes = -1; + + var timeSeparator = this._get(inst, 'timeSeparator'), + amPmText = this._get(inst, 'amPmText'), + showHours = this._get(inst, 'showHours'), + showMinutes = this._get(inst, 'showMinutes'), + optionalMinutes = this._get(inst, 'optionalMinutes'), + showPeriod = (this._get(inst, 'showPeriod') == true), + p = timeVal.indexOf(timeSeparator); + + // check if time separator found + if (p != -1) { + retVal.hours = parseInt(timeVal.substr(0, p), 10); + retVal.minutes = parseInt(timeVal.substr(p + 1), 10); + } + // check for hours only + else if ( (showHours) && ( !showMinutes || optionalMinutes ) ) { + retVal.hours = parseInt(timeVal, 10); + } + // check for minutes only + else if ( ( ! showHours) && (showMinutes) ) { + retVal.minutes = parseInt(timeVal, 10); + } + + if (showHours) { + var timeValUpper = timeVal.toUpperCase(); + if ((retVal.hours < 12) && (showPeriod) && (timeValUpper.indexOf(amPmText[1].toUpperCase()) != -1)) { + retVal.hours += 12; + } + // fix for 12 AM + if ((retVal.hours == 12) && (showPeriod) && (timeValUpper.indexOf(amPmText[0].toUpperCase()) != -1)) { + retVal.hours = 0; + } + } + + return retVal; + }, + + selectNow: function(event) { + var id = $(event.target).attr("data-timepicker-instance-id"), + $target = $(id), + inst = this._getInst($target[0]); + //if (!inst || (input && inst != $.data(input, PROP_NAME))) { return; } + var currentTime = new Date(); + inst.hours = currentTime.getHours(); + inst.minutes = currentTime.getMinutes(); + this._updateSelectedValue(inst); + this._updateTimepicker(inst); + this._hideTimepicker(); + }, + + deselectTime: function(event) { + var id = $(event.target).attr("data-timepicker-instance-id"), + $target = $(id), + inst = this._getInst($target[0]); + inst.hours = -1; + inst.minutes = -1; + this._updateSelectedValue(inst); + this._hideTimepicker(); + }, + + + selectHours: function (event) { + var $td = $(event.currentTarget), + id = $td.attr("data-timepicker-instance-id"), + newHours = parseInt($td.attr("data-hour")), + fromDoubleClick = event.data.fromDoubleClick, + $target = $(id), + inst = this._getInst($target[0]), + showMinutes = (this._get(inst, 'showMinutes') == true); + + // don't select if disabled + if ( $.timepicker._isDisabledTimepicker($target.attr('id')) ) { return false } + + $td.parents('.ui-timepicker-hours:first').find('a').removeClass('ui-state-active'); + $td.children('a').addClass('ui-state-active'); + inst.hours = newHours; + + // added for onMinuteShow callback + var onMinuteShow = this._get(inst, 'onMinuteShow'); + if (onMinuteShow) { + // this will trigger a callback on selected hour to make sure selected minute is allowed. + this._updateMinuteDisplay(inst); + } + + this._updateSelectedValue(inst); + + inst._hoursClicked = true; + if ((inst._minutesClicked) || (fromDoubleClick) || (showMinutes == false)) { + $.timepicker._hideTimepicker(); + } + // return false because if used inline, prevent the url to change to a hashtag + return false; + }, + + selectMinutes: function (event) { + var $td = $(event.currentTarget), + id = $td.attr("data-timepicker-instance-id"), + newMinutes = parseInt($td.attr("data-minute")), + fromDoubleClick = event.data.fromDoubleClick, + $target = $(id), + inst = this._getInst($target[0]), + showHours = (this._get(inst, 'showHours') == true); + + // don't select if disabled + if ( $.timepicker._isDisabledTimepicker($target.attr('id')) ) { return false } + + $td.parents('.ui-timepicker-minutes:first').find('a').removeClass('ui-state-active'); + $td.children('a').addClass('ui-state-active'); + + inst.minutes = newMinutes; + this._updateSelectedValue(inst); + + inst._minutesClicked = true; + if ((inst._hoursClicked) || (fromDoubleClick) || (showHours == false)) { + $.timepicker._hideTimepicker(); + // return false because if used inline, prevent the url to change to a hashtag + return false; + } + + // return false because if used inline, prevent the url to change to a hashtag + return false; + }, + + _updateSelectedValue: function (inst) { + var newTime = this._getParsedTime(inst); + if (inst.input) { + inst.input.val(newTime); + inst.input.trigger('change'); + } + var onSelect = this._get(inst, 'onSelect'); + if (onSelect) { onSelect.apply((inst.input ? inst.input[0] : null), [newTime, inst]); } // trigger custom callback + this._updateAlternate(inst, newTime); + return newTime; + }, + + /* this function process selected time and return it parsed according to instance options */ + _getParsedTime: function(inst) { + + if (inst.hours == -1 && inst.minutes == -1) { + return ''; + } + + // default to 0 AM if hours is not valid + if ((inst.hours < inst.hours.starts) || (inst.hours > inst.hours.ends )) { inst.hours = 0; } + // default to 0 minutes if minute is not valid + if ((inst.minutes < inst.minutes.starts) || (inst.minutes > inst.minutes.ends)) { inst.minutes = 0; } + + var period = "", + showPeriod = (this._get(inst, 'showPeriod') == true), + showLeadingZero = (this._get(inst, 'showLeadingZero') == true), + showHours = (this._get(inst, 'showHours') == true), + showMinutes = (this._get(inst, 'showMinutes') == true), + optionalMinutes = (this._get(inst, 'optionalMinutes') == true), + amPmText = this._get(inst, 'amPmText'), + selectedHours = inst.hours ? inst.hours : 0, + selectedMinutes = inst.minutes ? inst.minutes : 0, + displayHours = selectedHours ? selectedHours : 0, + parsedTime = ''; + + if (showPeriod) { + if (inst.hours == 0) { + displayHours = 12; + } + if (inst.hours < 12) { + period = amPmText[0]; + } + else { + period = amPmText[1]; + if (displayHours > 12) { + displayHours -= 12; + } + } + } + + var h = displayHours.toString(); + if (showLeadingZero && (displayHours < 10)) { h = '0' + h; } + + var m = selectedMinutes.toString(); + if (selectedMinutes < 10) { m = '0' + m; } + + if (showHours) { + parsedTime += h; + } + if (showHours && showMinutes && (!optionalMinutes || m != 0)) { + parsedTime += this._get(inst, 'timeSeparator'); + } + if (showMinutes && (!optionalMinutes || m != 0)) { + parsedTime += m; + } + if (showHours) { + if (period.length > 0) { parsedTime += this._get(inst, 'periodSeparator') + period; } + } + + return parsedTime; + }, + + /* Update any alternate field to synchronise with the main field. */ + _updateAlternate: function(inst, newTime) { + var altField = this._get(inst, 'altField'); + if (altField) { // update alternate field too + $(altField).each(function(i,e) { + $(e).val(newTime); + }); + } + }, + + /* This might look unused but it's called by the $.fn.timepicker function with param getTime */ + /* added v 0.2.3 - gitHub issue #5 - Thanks edanuff */ + _getTimeTimepicker : function(input) { + var inst = this._getInst(input); + return this._getParsedTime(inst); + }, + _getHourTimepicker: function(input) { + var inst = this._getInst(input); + if ( inst == undefined) { return -1; } + return inst.hours; + }, + _getMinuteTimepicker: function(input) { + var inst= this._getInst(input); + if ( inst == undefined) { return -1; } + return inst.minutes; + } + + }); + + + + /* Invoke the timepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new timepicker functionality + @return jQuery object */ + $.fn.timepicker = function (options) { + + /* Initialise the time picker. */ + if (!$.timepicker.initialized) { + $(document).mousedown($.timepicker._checkExternalClick). + find('body').append($.timepicker.tpDiv); + $.timepicker.initialized = true; + } + + + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options == 'string' && (options == 'getTime' || options == 'getHour' || options == 'getMinute' )) + return $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this[0]].concat(otherArgs)); + if (options == 'option' && arguments.length == 2 && typeof arguments[1] == 'string') + return $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this[0]].concat(otherArgs)); + return this.each(function () { + typeof options == 'string' ? + $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this].concat(otherArgs)) : + $.timepicker._attachTimepicker(this, options); + }); + }; + + /* jQuery extend now ignores nulls! */ + function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) + if (props[name] == null || props[name] == undefined) + target[name] = props[name]; + return target; + }; + + $.timepicker = new Timepicker(); // singleton instance + $.timepicker.initialized = false; + $.timepicker.uuid = new Date().getTime(); + $.timepicker.version = "0.3.1"; + + // Workaround for #4055 + // Add another global to avoid noConflict issues with inline event handlers + window['TP_jQuery_' + tpuuid] = $; + +})(jQuery); diff --git a/3rdparty/js/tipsy/MIT License b/3rdparty/js/tipsy/MIT License new file mode 100644 index 000000000..cd9802242 --- /dev/null +++ b/3rdparty/js/tipsy/MIT License @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2008 Jason Frame (jason@onehackoranother.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/3rdparty/js/tipsy/tipsy.js b/3rdparty/js/tipsy/tipsy.js new file mode 100644 index 000000000..f95c063fd --- /dev/null +++ b/3rdparty/js/tipsy/tipsy.js @@ -0,0 +1,258 @@ +// tipsy, facebook style tooltips for jquery +// version 1.0.0a +// (c) 2008-2010 jason frame [jason@onehackoranother.com] +// released under the MIT license + +(function($) { + + function maybeCall(thing, ctx) { + return (typeof thing == 'function') ? (thing.call(ctx)) : thing; + }; + + function isElementInDOM(ele) { + while (ele = ele.parentNode) { + if (ele == document) return true; + } + return false; + }; + + function Tipsy(element, options) { + this.$element = $(element); + this.options = options; + this.enabled = true; + this.fixTitle(); + }; + + Tipsy.prototype = { + show: function() { + var title = this.getTitle(); + if (title && this.enabled) { + var $tip = this.tip(); + + $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title); + $tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity + $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body); + + var pos = $.extend({}, this.$element.offset(), { + width: this.$element[0].offsetWidth, + height: this.$element[0].offsetHeight + }); + + var actualWidth = $tip[0].offsetWidth, + actualHeight = $tip[0].offsetHeight, + gravity = maybeCall(this.options.gravity, this.$element[0]); + + var tp; + switch (gravity.charAt(0)) { + case 'n': + tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; + break; + case 's': + tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; + break; + case 'e': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset}; + break; + case 'w': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset}; + break; + } + + if (gravity.length == 2) { + if (gravity.charAt(1) == 'w') { + tp.left = pos.left + pos.width / 2 - 15; + } else { + tp.left = pos.left + pos.width / 2 - actualWidth + 15; + } + } + + $tip.css(tp).addClass('tipsy-' + gravity); + $tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0); + if (this.options.className) { + $tip.addClass(maybeCall(this.options.className, this.$element[0])); + } + + if (this.options.fade) { + $tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity}); + } else { + $tip.css({visibility: 'visible', opacity: this.options.opacity}); + } + } + }, + + hide: function() { + if (this.options.fade) { + this.tip().stop().fadeOut(function() { $(this).remove(); }); + } else { + this.tip().remove(); + } + }, + + fixTitle: function() { + var $e = this.$element; + if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') { + $e.attr('original-title', $e.attr('title') || '').removeAttr('title'); + } + }, + + getTitle: function() { + var title, $e = this.$element, o = this.options; + this.fixTitle(); + var title, o = this.options; + if (typeof o.title == 'string') { + title = $e.attr(o.title == 'title' ? 'original-title' : o.title); + } else if (typeof o.title == 'function') { + title = o.title.call($e[0]); + } + title = ('' + title).replace(/(^\s*|\s*$)/, ""); + return title || o.fallback; + }, + + tip: function() { + if (!this.$tip) { + this.$tip = $('
').html('
'); + this.$tip.data('tipsy-pointee', this.$element[0]); + } + return this.$tip; + }, + + validate: function() { + if (!this.$element[0].parentNode) { + this.hide(); + this.$element = null; + this.options = null; + } + }, + + enable: function() { this.enabled = true; }, + disable: function() { this.enabled = false; }, + toggleEnabled: function() { this.enabled = !this.enabled; } + }; + + $.fn.tipsy = function(options) { + + if (options === true) { + return this.data('tipsy'); + } else if (typeof options == 'string') { + var tipsy = this.data('tipsy'); + if (tipsy) tipsy[options](); + return this; + } + + options = $.extend({}, $.fn.tipsy.defaults, options); + + function get(ele) { + var tipsy = $.data(ele, 'tipsy'); + if (!tipsy) { + tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options)); + $.data(ele, 'tipsy', tipsy); + } + return tipsy; + } + + function enter() { + var tipsy = get(this); + tipsy.hoverState = 'in'; + if (options.delayIn == 0) { + tipsy.show(); + } else { + tipsy.fixTitle(); + setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn); + } + }; + + function leave() { + var tipsy = get(this); + tipsy.hoverState = 'out'; + if (options.delayOut == 0) { + tipsy.hide(); + } else { + setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut); + } + }; + + if (!options.live) this.each(function() { get(this); }); + + if (options.trigger != 'manual') { + var binder = options.live ? 'live' : 'bind', + eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus', + eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur'; + this[binder](eventIn, enter)[binder](eventOut, leave); + } + + return this; + + }; + + $.fn.tipsy.defaults = { + className: null, + delayIn: 0, + delayOut: 0, + fade: false, + fallback: '', + gravity: 'n', + html: false, + live: false, + offset: 0, + opacity: 0.8, + title: 'title', + trigger: 'hover' + }; + + $.fn.tipsy.revalidate = function() { + $('.tipsy').each(function() { + var pointee = $.data(this, 'tipsy-pointee'); + if (!pointee || !isElementInDOM(pointee)) { + $(this).remove(); + } + }); + }; + + // Overwrite this method to provide options on a per-element basis. + // For example, you could store the gravity in a 'tipsy-gravity' attribute: + // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' }); + // (remember - do not modify 'options' in place!) + $.fn.tipsy.elementOptions = function(ele, options) { + return $.metadata ? $.extend({}, options, $(ele).metadata()) : options; + }; + + $.fn.tipsy.autoNS = function() { + return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n'; + }; + + $.fn.tipsy.autoWE = function() { + return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w'; + }; + + /** + * yields a closure of the supplied parameters, producing a function that takes + * no arguments and is suitable for use as an autogravity function like so: + * + * @param margin (int) - distance from the viewable region edge that an + * element should be before setting its tooltip's gravity to be away + * from that edge. + * @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer + * if there are no viewable region edges effecting the tooltip's + * gravity. It will try to vary from this minimally, for example, + * if 'sw' is preferred and an element is near the right viewable + * region edge, but not the top edge, it will set the gravity for + * that element's tooltip to be 'se', preserving the southern + * component. + */ + $.fn.tipsy.autoBounds = function(margin, prefer) { + return function() { + var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)}, + boundTop = $(document).scrollTop() + margin, + boundLeft = $(document).scrollLeft() + margin, + $this = $(this); + + if ($this.offset().top < boundTop) dir.ns = 'n'; + if ($this.offset().left < boundLeft) dir.ew = 'w'; + if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e'; + if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's'; + + return dir.ns + (dir.ew ? dir.ew : ''); + } + }; + +})(jQuery); diff --git a/3rdparty/js/underscore/underscore.js b/3rdparty/js/underscore/underscore.js new file mode 100644 index 000000000..c1d9d3aed --- /dev/null +++ b/3rdparty/js/underscore/underscore.js @@ -0,0 +1 @@ +(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.4";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2),e=w.isFunction(t);return w.map(n,function(n){return(e?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t,r){return w.isEmpty(t)?r?null:[]:w[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.findWhere=function(n,t){return w.where(n,t,!0)},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var k=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=k(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.indexi;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i},w.bind=function(n,t){if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));var r=o.call(arguments,2);return function(){return n.apply(t,r.concat(o.call(arguments)))}},w.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},w.bindAll=function(n){var t=o.call(arguments,1);return 0===t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var I=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=I(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&I(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return I(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),"function"!=typeof/./&&(w.isFunction=function(n){return"function"==typeof n}),w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return n===void 0},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var M={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};M.unescape=w.invert(M.escape);var S={escape:RegExp("["+w.keys(M.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(M.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(S[n],function(t){return M[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),D.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=++N+"";return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var T=/(.)^/,q={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},B=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){var e;r=w.defaults({},r,w.templateSettings);var u=RegExp([(r.escape||T).source,(r.interpolate||T).source,(r.evaluate||T).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(B,function(n){return"\\"+q[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,w);var c=function(n){return e.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},w.chain=function(n){return w(n).chain()};var D=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],D.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return D.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this); \ No newline at end of file diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 000000000..e69de29bb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/README.md b/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/api/apiexception.php b/api/apiexception.php new file mode 100644 index 000000000..e69de29bb diff --git a/api/calendar.php b/api/calendar.php new file mode 100644 index 000000000..3906cbfed --- /dev/null +++ b/api/calendar.php @@ -0,0 +1,12 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\PublicApi; + +class Calendar { + +} \ No newline at end of file diff --git a/api/events.php b/api/events.php new file mode 100644 index 000000000..61abce55f --- /dev/null +++ b/api/events.php @@ -0,0 +1,12 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\PublicApi; + +class Events extends ObjectType { + +} \ No newline at end of file diff --git a/api/journals.php b/api/journals.php new file mode 100644 index 000000000..7c8414897 --- /dev/null +++ b/api/journals.php @@ -0,0 +1,12 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\PublicApi; + +class Journals extends ObjectType { + +} \ No newline at end of file diff --git a/api/legacy/app.php b/api/legacy/app.php new file mode 100644 index 000000000..1583bf44f --- /dev/null +++ b/api/legacy/app.php @@ -0,0 +1,4 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\PublicApi; + +class Objects { + +} \ No newline at end of file diff --git a/api/objectstype.php b/api/objectstype.php new file mode 100644 index 000000000..28d2b94e2 --- /dev/null +++ b/api/objectstype.php @@ -0,0 +1,12 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\PublicApi; + +abstract class ObjectType { + +} \ No newline at end of file diff --git a/api/todos.php b/api/todos.php new file mode 100644 index 000000000..71fb766e9 --- /dev/null +++ b/api/todos.php @@ -0,0 +1,12 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\PublicApi; + +class Todos extends ObjectType { + +} \ No newline at end of file diff --git a/appinfo/app.php b/appinfo/app.php new file mode 100644 index 000000000..90c7f74cd --- /dev/null +++ b/appinfo/app.php @@ -0,0 +1,22 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar; + +require_once(__DIR__ . '/../3rdparty/VObject/includes.php'); + +define('OCA\Calendar\JSON_API_VERSION', '1.0'); +define('OCA\Calendar\PHP_API_VERSION', '1.0'); + +$app = new App(); +$app->registerNavigation(); + +$app->registerCron(); +$app->registerHooks(); +$app->registerProviders(); + +Sabre\VObject\Document::$propertyMap['DateTime'] = '\OCA\Calendar\CustomSabre\Property\DateTime'; \ No newline at end of file diff --git a/appinfo/classpath.php b/appinfo/classpath.php new file mode 100644 index 000000000..37bcb78f1 --- /dev/null +++ b/appinfo/classpath.php @@ -0,0 +1,99 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +\OC::$CLASSPATH['OCA\Calendar\App'] = 'calendar/lib/app.php'; + +\OC::$CLASSPATH['OCA\Calendar\Backend\Anniversary'] = 'calendar/backend/anniversary.php'; +\OC::$CLASSPATH['OCA\Calendar\Backend\Backend'] = 'calendar/backend/backend.php'; +\OC::$CLASSPATH['OCA\Calendar\Backend\Birthday'] = 'calendar/backend/birthday.php'; +\OC::$CLASSPATH['OCA\Calendar\Backend\CalDAV'] = 'calendar/backend/caldav.php'; +\OC::$CLASSPATH['OCA\Calendar\Backend\IBackend'] = 'calendar/backend/ibackend.php'; +\OC::$CLASSPATH['OCA\Calendar\Backend\Local'] = 'calendar/backend/local.php'; +\OC::$CLASSPATH['OCA\Calendar\Backend\LocalStorage'] = 'calendar/backend/localstorage.php'; +\OC::$CLASSPATH['OCA\Calendar\Backend\Sharing'] = 'calendar/backend/sharing.php'; +\OC::$CLASSPATH['OCA\Calendar\Backend\WebCal'] = 'calendar/backend/webcal.php'; + +\OC::$CLASSPATH['OCA\Calendar\Backgroundjob\Task'] = 'calendar/backgroundjob/task.php'; + +\OC::$CLASSPATH['OCA\Calendar\BusinessLayer\BusinessLayer'] = 'calendar/businesslayer/businesslayer.php'; +\OC::$CLASSPATH['OCA\Calendar\BusinessLayer\CalendarBusinessLayer'] = 'calendar/businesslayer/calendarbusinesslayer.php'; +\OC::$CLASSPATH['OCA\Calendar\BusinessLayer\ObjectBusinessLayer'] = 'calendar/businesslayer/objectbusinesslayer.php'; + +\OC::$CLASSPATH['OCA\Calendar\Controller\Controller'] = 'calendar/controller/controller.php'; +\OC::$CLASSPATH['OCA\Calendar\Controller\CalendarController'] = 'calendar/controller/calendarcontroller.php'; +\OC::$CLASSPATH['OCA\Calendar\Controller\ObjectController'] = 'calendar/controller/objectcontroller.php'; +\OC::$CLASSPATH['OCA\Calendar\Controller\ObjectTypeController'] = 'calendar/controller/objecttypecontroller.php'; +\OC::$CLASSPATH['OCA\Calendar\Controller\EventController'] = 'calendar/controller/eventcontroller.php'; +\OC::$CLASSPATH['OCA\Calendar\Controller\JournalController'] = 'calendar/controller/journalcontroller.php'; +\OC::$CLASSPATH['OCA\Calendar\Controller\TodoController'] = 'calendar/controller/todocontroller.php'; +\OC::$CLASSPATH['OCA\Calendar\Controller\SettingsController'] = 'calendar/controller/settingscontroller.php'; +\OC::$CLASSPATH['OCA\Calendar\Controller\ViewController'] = 'calendar/controller/viewcontroller.php'; + +\OC::$CLASSPATH['OCA\Calendar\Db\Entity'] = 'calendar/db/entity.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\Collection'] = 'calendar/db/collection.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\Mapper'] = 'calendar/db/mapper.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\Backend'] = 'calendar/db/backend.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\BackendCollection'] = 'calendar/db/backendcollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\BackendMapper'] = 'calendar/db/backendmapper.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\Calendar'] = 'calendar/db/calendar.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\CalendarCollection'] = 'calendar/db/calendarcollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\CalendarMapper'] = 'calendar/db/calendarmapper.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\Object'] = 'calendar/db/object.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\ObjectCollection'] = 'calendar/db/objectcollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\ObjectMapper'] = 'calendar/db/objectmapper.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\Timezone'] = 'calendar/db/timezone.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\TimezoneCollection'] = 'calendar/db/timezonecollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\ObjectType'] = 'calendar/db/objecttype.php'; +\OC::$CLASSPATH['OCA\Calendar\Db\Permissions'] = 'calendar/db/permissions.php'; + +\OC::$CLASSPATH['OCA\Calendar\Http\JSONResponse'] = 'calendar/http/jsonresponse.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\IResponse'] = 'calendar/http/iresponse.php'; + +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICS'] = 'calendar/http/ics/ics.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSCollection'] = 'calendar/http/ics/icscollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSReader'] = 'calendar/http/ics/icsreader.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSCalendar'] = 'calendar/http/ics/calendar.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSCalendarCollection'] = 'calendar/http/ics/calendarcollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSCalendarReader'] = 'calendar/http/ics/calendarreader.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSObject'] = 'calendar/http/ics/object.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSObjectCollection'] = 'calendar/http/ics/objectcollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSObjectReader'] = 'calendar/http/ics/objectreader.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSTimezone'] = 'calendar/http/ics/timezone.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSTimezoneCollection'] = 'calendar/http/ics/timzonecollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\ICS\ICSTimezoneReader'] = 'calendar/http/ics/timezonereader.php'; + +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSON'] = 'calendar/http/json/json.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONCollection'] = 'calendar/http/json/jsoncollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONReader'] = 'calendar/http/json/jsonreader.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONCalendar'] = 'calendar/http/json/calendar.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONCalendarCollection'] = 'calendar/http/json/calendarcollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONCalendarReader'] = 'calendar/http/json/calendarreader.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONObject'] = 'calendar/http/json/object.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONObjectCollection'] = 'calendar/http/json/objectcollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONObjectReader'] = 'calendar/http/json/objectreader.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONTimezone'] = 'calendar/http/json/timezone.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONTimezoneCollection'] = 'calendar/http/json/timezonecollection.php'; +\OC::$CLASSPATH['OCA\Calendar\Http\JSON\JSONTimezoneReader'] = 'calendar/http/json/timezonereader.php'; + +\OC::$CLASSPATH['OCA\Calendar\SabreProperty\DateTime'] = 'calendar/sabre/property/datetime.php'; + +//caldav implementation +/*\OC::$CLASSPATH['OCA\Calendar\Sabre'] = ''; +\OC::$CLASSPATH['OCA\Calendar\Sabre'] = ''; +\OC::$CLASSPATH['OCA\Calendar\Sabre'] = ''; +\OC::$CLASSPATH['OCA\Calendar\Sabre'] = ''; +\OC::$CLASSPATH['OCA\Calendar\Sabre'] = ''; +\OC::$CLASSPATH['OCA\Calendar\Sabre'] = '';*/ + +\OC::$CLASSPATH['OCA\Calendar\Utility\Utility'] = 'calendar/utility/utility.php'; +\OC::$CLASSPATH['OCA\Calendar\Utility\BackendUtility'] = 'calendar/utility/backend.php'; +\OC::$CLASSPATH['OCA\Calendar\Utility\CalendarUtility'] = 'calendar/utility/calendar.php'; +\OC::$CLASSPATH['OCA\Calendar\Utility\JSONUtility'] = 'calendar/utility/json.php'; +\OC::$CLASSPATH['OCA\Calendar\Utility\ObjectUtility'] = 'calendar/utility/object.php'; +\OC::$CLASSPATH['OCA\Calendar\Utility\SabreUtility'] = 'calendar/utility/sabre.php'; +\OC::$CLASSPATH['OCA\Calendar\Utility\UpdateUtility'] = 'calendar/utility/update.php'; \ No newline at end of file diff --git a/appinfo/database.xml b/appinfo/database.xml new file mode 100644 index 000000000..0bf783929 --- /dev/null +++ b/appinfo/database.xml @@ -0,0 +1,392 @@ + + + *dbname* + true + false + utf8 + + + *dbprefix*clndr_calcache + + + id + integer + 0 + true + 1 + true + 4 + + + user_id + text + + true + 255 + + + owner_id + text + + true + 255 + + + backend + text + + true + 255 + + + uri + text + + true + 255 + + + displayname + text + + true + 255 + + + components + text + VEVENT + false + 100 + + + ctag + integer + 0 + true + true + 4 + + + timezone + text + false + + + color + text + #000000 + false + 10 + + + order + integer + 0 + true + true + 4 + + + enabled + boolean + 1 + true + + + cruds + integer + 31 + true + + +
+ + + *dbprefix*clndr_objcache + + + id + integer + 0 + true + 1 + true + 4 + + + calendarid + integer + + true + true + 4 + + + uri + text + + true + 255 + + + type + text + + true + 40 + + + startdate + timestamp + 1970-01-01 00:00:00 + false + + + enddate + timestamp + 1970-01-01 00:00:00 + false + + + timezone + text + false + + + repeating + boolean + 0 + false + + + lastoccurence + timestamp + 1970-01-01 00:00:00 + false + + + summary + text + + false + + + calendardata + clob + false + + + lastmodified + integer + + false + 4 + + + etag + text + + false + 40 + + +
+ + + *dbprefix*clndr_repeatcache + + + id + integer + 0 + true + 1 + true + 4 + + + calendarid + integer + + true + true + 4 + + + uri + text + + true + 255 + + + startdate + timestamp + 1970-01-01 00:00:00 + false + + + enddate + timestamp + 1970-01-01 00:00:00 + false + + +
+ + + *dbprefix*clndr_calendars + + + + id + integer + 0 + true + 1 + true + 4 + + + userid + text + + false + 255 + + + displayname + text + + false + 100 + + + uri + text + + false + 255 + + + active + integer + 1 + true + 4 + + + ctag + integer + 0 + true + true + 4 + + + calendarorder + integer + 0 + true + true + 4 + + + calendarcolor + text + + false + 10 + + + timezone + text + false + + + components + text + + false + 100 + + +
+ + + *dbprefix*clndr_objects + + + + id + integer + 0 + true + 1 + true + 4 + + + calendarid + integer + + true + true + 4 + + + objecttype + text + + true + 40 + + + startdate + timestamp + 1970-01-01 00:00:00 + false + + + enddate + timestamp + 1970-01-01 00:00:00 + false + + + repeating + integer + + false + 4 + + + summary + text + + false + 255 + + + calendardata + clob + false + + + uri + text + + false + 255 + + + lastmodified + integer + + false + 4 + + +
+
\ No newline at end of file diff --git a/appinfo/info.xml b/appinfo/info.xml new file mode 100644 index 000000000..debb88953 --- /dev/null +++ b/appinfo/info.xml @@ -0,0 +1,19 @@ + + + calendar + Calendar + AGPL + Georg Ehrke, Bart Visscher, Jakob Sack + 6.90 + true + Calendar with CalDAV support + + + sabre/main.php + sabre/main.php + + + sharing/public.php + sharing/public.php + + \ No newline at end of file diff --git a/appinfo/migrate.php b/appinfo/migrate.php new file mode 100644 index 000000000..fd9c663fc --- /dev/null +++ b/appinfo/migrate.php @@ -0,0 +1,70 @@ +'calendar_calendars', + 'matchcol'=>'userid', + 'matchval'=>$this->uid, + 'idcol'=>'id' + ); + $ids = $this->content->copyRows( $options ); + + $options = array( + 'table'=>'calendar_objects', + 'matchcol'=>'calendarid', + 'matchval'=>$ids + ); + + // Export tags + $ids2 = $this->content->copyRows( $options ); + + // If both returned some ids then they worked + if(is_array($ids) && is_array($ids2)) { + return true; + } else { + return false; + } + + } + + // Import function for calendar + function import( ) { + switch( $this->appinfo->version ) { + default: + // All versions of the app have had the same db structure, so all can use the same import function + $query = $this->content->prepare( 'SELECT * FROM `calendar_calendars` WHERE `userid` = ?' ); + $results = $query->execute( array( $this->olduid ) ); + $idmap = array(); + while( $row = $results->fetchRow() ) { + // Import each calendar + $calendarquery = OCP\DB::prepare( 'INSERT INTO `*PREFIX*calendar_calendars` (`userid`,`displayname`,`uri`,`ctag`,`calendarorder`,`calendarcolor`,`timezone`,`components`) VALUES(?,?,?,?,?,?,?,?)' ); + $calendarquery->execute(array( $this->uid, $row['displayname'], $row['uri'], $row['ctag'], $row['calendarorder'], $row['calendarcolor'], $row['timezone'], $row['components'])); + // Map the id + $idmap[$row['id']] = OCP\DB::insertid('*PREFIX*calendar_calendars'); + // Make the calendar active + OC_Calendar_Calendar::setCalendarActive($idmap[$row['id']], true); + } + // Now tags + foreach($idmap as $oldid => $newid) { + + $query = $this->content->prepare( 'SELECT * FROM `calendar_objects` WHERE `calendarid` = ?' ); + $results = $query->execute( array( $oldid ) ); + while( $row = $results->fetchRow() ) { + // Import the objects + $objectquery = OCP\DB::prepare( 'INSERT INTO `*PREFIX*calendar_objects` (`calendarid`,`objecttype`,`startdate`,`enddate`,`repeating`,`summary`,`calendardata`,`uri`,`lastmodified`) VALUES(?,?,?,?,?,?,?,?,?)' ); + $objectquery->execute(array( $newid, $row['objecttype'], $row['startdate'], $row['enddate'], $row['repeating'], $row['summary'], $row['calendardata'], $row['uri'], $row['lastmodified'] )); + } + } + // All done! + break; + } + + return true; + } + +} + +// Load the provider +new OC_Migration_Provider_Calendar( 'calendar' ); \ No newline at end of file diff --git a/appinfo/routes.php b/appinfo/routes.php new file mode 100644 index 000000000..9c5ef9a45 --- /dev/null +++ b/appinfo/routes.php @@ -0,0 +1,43 @@ +create('calendar.view.index', '/')->action(function($params){ + $app = new \OCA\Calendar\App($params); + $app->dispatch('ViewController', 'index'); +}); + +$this->create('calendar.settings.getView', '/getView')->get()->action(function($params){ + $app = new \OCA\Calendar\App($params); + $app->dispatch('SettingsController', 'getView'); +}); + +$this->create('calendar.settings.setView', '/setView/{view}')->get()->action(function($params){ + $app = new \OCA\Calendar\App($params); + $app->dispatch('SettingsController', 'setView'); +}); + +$this->create('calendar.calendars.forceUpdate', '/v1/calendars-forceUpdate')->action(function($params){ + $app = new \OCA\Calendar\App($params); + $app->dispatch('CalendarController', 'forceUpdate'); +}); + +//set up resources +$routes = array( + 'resources' => array( + 'calendar' => array('url' => '/v1/calendars'), + 'object' => array('url' => '/v1/calendars/{calendarId}/objects'), + 'event' => array('url' => '/v1/calendars/{calendarId}/events'), + 'journal' => array('url' => '/v1/calendars/{calendarId}/journals'), + 'todo' => array('url' => '/v1/calendars/{calendarId}/todos'), + ) +); + +$a = new \OCA\Calendar\App(); +$a->registerRoutes($this, $routes); \ No newline at end of file diff --git a/appinfo/update.php b/appinfo/update.php new file mode 100644 index 000000000..8e9e7639d --- /dev/null +++ b/appinfo/update.php @@ -0,0 +1,37 @@ + 'local', + 'classname' => '\OCA\Calendar\Backend\Local', + 'arguments' => '', + 'enabled' => true, + ),/* + array ( + 'backend' => 'WebCal', + 'classname' => '\OCA\Calendar\Backend\WebCal', + 'arguments' => '', + 'enabled' => false + ),*/ + + ); + \OCP\Config::setSystemValue('calendar_backends', $backends); + + //fix calendars: + $users = \OCP\User::getUsers(); + foreach($users as $userId) { + $userstimezone = OCP\Config::getUserValue($userId, 'calendar', 'timezone', date_default_timezone_get()); + $stmt = OCP\DB::prepare('UPDATE `*PREFIX*clndr_calendars` SET `timezone`=? WHERE `userid`=?'); + $stmt->execute(array($userstimezone, $userId)); + //how to delete config values ??? + } + + //there was no way set which calendar supports what kind of component, so we can set all calendars to support all components. + $stmtComponents = OCP\DB::prepare('UPDATE `*PREFIX*clndr_calendars` SET `components`=?'); + $stmtComponents->execute(array((string) ObjectType::ALL)); +} \ No newline at end of file diff --git a/appinfo/version b/appinfo/version new file mode 100644 index 000000000..a98881557 --- /dev/null +++ b/appinfo/version @@ -0,0 +1 @@ +0.10.7 \ No newline at end of file diff --git a/backend/anniversary.php b/backend/anniversary.php new file mode 100644 index 000000000..fcb509826 --- /dev/null +++ b/backend/anniversary.php @@ -0,0 +1,164 @@ + + * Copyright (c) 2014 Thomas Tanghus + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backend; + +use \OCA\Calendar\AppFramework\Core\API; +use \OCA\Calendar\AppFramework\Db\Mapper; +use \OCA\Calendar\AppFramework\Db\DoesNotExistException; +use \OCA\Calendar\AppFramework\Db\MultipleObjectsReturnedException; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\CalendarCollection; +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectCollection; +use \OCA\Calendar\Db\TimeZone; +use \OCA\Calendar\Db\TimeZoneCollection; +use \OCA\Calendar\Db\ObjectType; + +use \OCA\Calendar\Db\Permissions; + +class Anniversary extends Backend { + + public $calendarURI; + + public function __construct($api, $parameters){ + parent::__construct($api, 'Anniversary'); + + $this->calendarURI = 'anniversary'; + } + + /** + * @brief returns whether or not a backend can be enabled + * @returns boolean + * + * This method returns a boolean. + * This method is mandatory! + */ + public function canBeEnabled() { + return true;//return \OCP\App::isEnabled('contacts'); + } + + /** + * @brief returns whether or not calendar objects should be cached + * @param string $calendarURI + * @param string $userId + * @returns boolean + * + * This method returns a boolen. + * This method is mandatory! + */ + public function cacheObjects($uri, $userId) { + return false; + } + + /** + * @brief returns information about calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $userId + * @returns array with \OCA\Calendar\Db\Calendar object + * @throws DoesNotExistException if uri does not exist + * + * This method returns an \OCA\Calendar\Db\Calendar object. + * This method is mandatory! + */ + public function findCalendar($calendarURI, $userId) { + if($calendarURI !== $this->calendarURI) { + $msg = 'Backend\Anniversary::findCalendar(): '; + $msg .= '"' . $calendarURI . '" doesn\'t exist'; + throw new DoesNotExistException($msg); + } + + $calendar = new Calendar(); + $calendar->setUserId($userId); + $calendar->setOwnerId($userId); + $calendar->setBackend($this->backend); + $calendar->setUri($this->calendarURI); + $calendar->setDisplayname('Anniversary'); //TODO - use translation + $calendar->setComponents(ObjectType::EVENT); + $calendar->setCtag(1); //sum of all addressbook ctags + $calendar->setTimezone(new TimeZone('UTC')); + $calendar->setCruds(Permissions::READ + Permissions::SHARE); + + return $calendar; + } + + /** + * @brief returns all calendars of the user $userId + * @param string $userId + * @returns \OCA\Calendar\Db\CalendarCollection + * @throws DoesNotExistException if uri does not exist + * + * This method returns an \OCA\Calendar\Db\CalendarCollection object. + * This method is mandatory! + */ + public function findCalendars($userId, $limit=null, $offset=null) { + return new CalendarCollection($this->findCalendar($this->calendarURI, $userId)); + } + + /** + * @brief returns number of calendar + * @param string $userid + * @returns integer + * + * This method returns an integer + * This method is mandatory! + */ + public function countCalendars($userId) { + return 1; + } + + /** + * @brief returns whether or not a calendar exists + * @param string $calendarURI + * @param string $userid + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function doesCalendarExist($calendarURI, $userId) { + if($calendarURI === $this->calendarURI) { + return true; + } else { + return false; + } + } + + /** + * @brief returns information about the object (event/journal/todo) with the uid $objectURI in the calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $objectURI + * @param string $userid + * @returns \OCA\Calendar\Db\Object object + * @throws DoesNotExistException if calendar does not exist + * @throws DoesNotExistException if object does not exist + * + * This method returns an \OCA\Calendar\Db\Object object. + * This method is mandatory! + */ + public function findObject(Calendar &$calendar, $objectURI) { + //TODO implement + throw new DoesNotExistException(); + } + + /** + * @brief returns all objects in the calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $userId + * @returns \OCA\Calendar\Db\ObjectCollection + * @throws DoesNotExistException if calendar does not exist + * + * This method returns an \OCA\Calendar\Db\ObjectCollection object. + * This method is mandatory! + */ + public function findObjects(Calendar &$calendar, $limit, $offset) { + //TODO implement + return new ObjectCollection(); + } +} \ No newline at end of file diff --git a/backend/backend.php b/backend/backend.php new file mode 100644 index 000000000..173c73fe4 --- /dev/null +++ b/backend/backend.php @@ -0,0 +1,331 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backend; + +use \OCP\AppFramework\IAppContainer; + +use \OCA\Calendar\Db\Calendar; + +use \OCA\Calendar\Db\ObjectType; +use \OCA\Calendar\Db\Permissions; + +//constants +define('OCA\Calendar\Backend\NOT_IMPLEMENTED', -501); +define('OCA\Calendar\Backend\CREATE_CALENDAR', 1); +define('OCA\Calendar\Backend\UPDATE_CALENDAR', 2); +define('OCA\Calendar\Backend\DELETE_CALENDAR', 4); +define('OCA\Calendar\Backend\MERGE_CALENDAR', 8); +define('OCA\Calendar\Backend\CREATE_OBJECT', 16); +define('OCA\Calendar\Backend\UPDATE_OBJECT', 32); +define('OCA\Calendar\Backend\DELETE_OBJECT', 64); +define('OCA\Calendar\Backend\FIND_IN_PERIOD', 128); +define('OCA\Calendar\Backend\FIND_OBJECTS_BY_TYPE', 256); +define('OCA\Calendar\Backend\FIND_IN_PERIOD_BY_TYPE', 521); +define('OCA\Calendar\Backend\SEARCH_BY_PROPERTIES', 1024); +define('OCA\Calendar\Backend\PROVIDES_CRON_SCRIPT', 2048); + +abstract class Backend implements IBackend { + + /** + * app container for dependency injection + * @var \OCP\AppFramework\IAppContainer + */ + protected $app; + + /** + * backend name + * @var string + */ + protected $backend; + + /** + * maps action-constants to method names + * @var arrray + */ + protected $possibleActions = array( + CREATE_CALENDAR => 'createCalendar', + UPDATE_CALENDAR => 'updateCalendar', + DELETE_CALENDAR => 'deleteCalendar', + MERGE_CALENDAR => 'mergeCalendar', + CREATE_OBJECT => 'createObject', + UPDATE_OBJECT => 'updateObject', + DELETE_OBJECT => 'deleteObject', + FIND_IN_PERIOD => 'findObjectsInPeriod', + FIND_OBJECTS_BY_TYPE => 'findObjectsByType', + FIND_IN_PERIOD_BY_TYPE => 'findObjectsByTypeInPeriod', + SEARCH_BY_PROPERTIES => 'searchByProperties', + ); + + /** + * Constructor + * @param \OCP\AppFramework\IAppContainer $api + * @param string $backendName + */ + public function __construct(IAppContainer $app, $backend=null){ + $this->app = $app; + + if($backend === null) { + $backend = get_class($this); + } + + $this->backend = strtolower($backend); + } + + /** + * @brief get integer that represents supported actions + * @returns integer + * + * This method returns an integer. + * This method is mandatory! + */ + public function getSupportedActions() { + $actions = 0; + foreach($this->possibleActions as $action => $methodName) { + if(method_exists($this, $methodName)) { + $actions |= $action; + } + } + + return $actions; + } + + /** + * @brief Check if backend implements actions + * @param string $actions + * @returns integer + * + * This method returns an integer. + * If the action is supported, it returns an integer that can be compared with \OC\Calendar\Backend\CREATE_CALENDAR, etc... + * If the action is not supported, it returns -501 + * This method is mandatory! + */ + public function implementsActions($actions) { + return (bool)($this->getSupportedActions() & $actions); + } + + /** + * @brief returns whether or not a backend can be enabled + * @returns boolean + * + * This method returns a boolean. + * This method is mandatory! + */ + public function canBeEnabled() { + return true; + } + + /** + * @brief returns whether or not calendar objects should be cached + * @param string $calendarURI + * @param string $userId + * @returns boolean + * + * This method returns a boolen. + * This method is mandatory! + */ + public function cacheObjects($calendarURI, $userId) { + return true; + } + + /** + * @brief returns information about calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $userId + * @returns array with \OCA\Calendar\Db\Calendar object + * @throws DoesNotExistException if uri does not exist + * + * This method returns an \OCA\Calendar\Db\Calendar object. + * This method is mandatory! + */ + abstract public function findCalendar($calendarURI, $userId); + + /** + * @brief returns all calendars of the user $userId + * @param string $userId + * @returns \OCA\Calendar\Db\CalendarCollection + * @throws DoesNotExistException if uri does not exist + * + * This method returns an \OCA\Calendar\Db\CalendarCollection object. + * This method is mandatory! + */ + abstract public function findCalendars($userId, $limit, $offset); + + /** + * @brief returns number of calendar + * @param string $userid + * @returns integer + * + * This method returns an integer + * This method is mandatory! + */ + public function countCalendars($userId) { + return $this->findCalendars($userId)->count(); + } + + /** + * @brief returns whether or not a calendar exists + * @param string $calendarURI + * @param string $userid + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function doesCalendarExist($calendarURI, $userId) { + try { + $this->findCalendar($calendarURI, $userId); + return true; + } catch (Exception $ex) { + return false; + } + } + + /** + * @brief returns ctag of a calendar + * @param string $calendarURI + * @param string $userid + * @returns integer + * @throws DoesNotExistException if calendar does not exist + * + * This method returns a integer + * This method is mandatory! + */ + public function getCalendarsCTag($calendarURI, $userId) { + $calendar = $this->findCalendar($calendarURI, $userId)->getCTag(); + } + + /** + * @brief returns information about the object (event/journal/todo) with the uid $objectURI in the calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $objectURI + * @param string $userid + * @returns \OCA\Calendar\Db\Object object + * @throws DoesNotExistException if calendar does not exist + * @throws DoesNotExistException if object does not exist + * + * This method returns an \OCA\Calendar\Db\Object object. + * This method is mandatory! + */ + abstract public function findObject(Calendar &$calendar, $objectURI); + + /** + * @brief returns all objects in the calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $userId + * @returns \OCA\Calendar\Db\ObjectCollection + * @throws DoesNotExistException if calendar does not exist + * + * This method returns an \OCA\Calendar\Db\ObjectCollection object. + * This method is mandatory! + */ + abstract public function findObjects(Calendar &$calendar, $limit, $offset); + + /** + * @brief returns number of objects in calendar + * @param string $calendarURI + * @param string $userid + * @returns integer + * @throws DoesNotExistException if calendar does not exist + * + * This method returns an integer + * This method is mandatory! + */ + public function countObjects(Calendar $calendar) { + return $this->findObjects($calendarURI, $userId)->count(); + } + + /** + * @brief returns whether or not an object exists + * @param string $calendarURI + * @param string $objectURI + * @param string $userid + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function doesObjectExist(Calendar $calendar, $objectURI) { + try { + $this->findObject($calendarURI, $objectURI, $userId); + return true; + } catch (Exception $ex) { + return false; + } + } + + /** + * @brief returns etag of an object + * @param string $calendarURI + * @param string $objectURI + * @param string $userid + * @returns string + * @throws DoesNotExistException if calendar does not exist + * @throws DoesNotExistException if object does not exist + * + * This method returns a string + * This method is mandatory! + */ + public function getObjectsETag(Calendar $calendar, $objectURI) { + return $this->find($calendarURI, $objectURI, $userId)->getEtag(); + } + + /** + * @brief returns whether or not a backend can store a calendar's color + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreColor() { + return false; + } + + /** + * @brief returns whether or not a backend can store a calendar's supported components + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreComponents() { + return false; + } + + /** + * @brief returns whether or not a backend can store a calendar's displayname + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreDisplayname() { + return false; + } + + /** + * @brief returns whether or not a backend can store if a calendar is enabled + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreEnabled() { + return false; + } + + /** + * @brief returns whether or not a backend can store a calendar's order + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreOrder() { + return false; + } +} \ No newline at end of file diff --git a/backend/backendexception.php b/backend/backendexception.php new file mode 100644 index 000000000..c13861af7 --- /dev/null +++ b/backend/backendexception.php @@ -0,0 +1,14 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backend; + +class BackendException extends \Exception { + const OTHER = -1; + const CACHEOUTDATED = 1; + const DOESNOTIMPLEMENT = 2; +} \ No newline at end of file diff --git a/backend/birthday.php b/backend/birthday.php new file mode 100644 index 000000000..5617c0dd4 --- /dev/null +++ b/backend/birthday.php @@ -0,0 +1,166 @@ + + * Copyright (c) 2014 Thomas Tanghus + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backend; + +use \OCA\Calendar\AppFramework\Core\API; +use \OCA\Calendar\AppFramework\Db\Mapper; +use \OCA\Calendar\AppFramework\Db\DoesNotExistException; +use \OCA\Calendar\AppFramework\Db\MultipleObjectsReturnedException; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\CalendarCollection; +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectCollection; +use \OCA\Calendar\Db\TimeZone; +use \OCA\Calendar\Db\TimeZoneCollection; +use \OCA\Calendar\Db\ObjectType; + +use \OCA\Calendar\Db\Permissions; + +class Birthday extends Backend { + + public $calendarURI; + + public function __construct($api, $parameters){ + parent::__construct($api, 'Birthday'); + + $this->calendarURI = 'birthday'; + } + + /** + * @brief returns whether or not a backend can be enabled + * @returns boolean + * + * This method returns a boolean. + * This method is mandatory! + */ + public function canBeEnabled() { + return true;//\OCP\App::isEnabled('contacts'); + } + + /** + * @brief returns whether or not calendar objects should be cached + * @param string $calendarURI + * @param string $userId + * @returns boolean + * + * This method returns a boolen. + * This method is mandatory! + */ + public function cacheObjects($uri, $userId) { + return false; + } + + /** + * @brief returns information about calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $userId + * @returns array with \OCA\Calendar\Db\Calendar object + * @throws DoesNotExistException if uri does not exist + * + * This method returns an \OCA\Calendar\Db\Calendar object. + * This method is mandatory! + */ + public function findCalendar($calendarURI, $userId) { + if($calendarURI !== $this->calendarURI) { + $msg = 'Backend\Birthday::findCalendar(): '; + $msg .= '"' . $calendarURI . '" doesn\'t exist'; + throw new DoesNotExistException($msg); + } + + $calendar = new Calendar(); + $calendar->setUserId($userId); + $calendar->setOwnerId($userId); + $calendar->setBackend($this->backend); + $calendar->setUri($this->calendarURI); + $calendar->setDisplayname('Birthday'); //TODO - use translation + $calendar->setComponents(ObjectType::EVENT); + $calendar->setCtag(1); //sum of all addressbook ctags + $calendar->setTimezone(new TimeZone('UTC')); + $calendar->setCruds(Permissions::READ + Permissions::SHARE); + + return $calendar; + } + + /** + * @brief returns all calendars of the user $userId + * @param string $userId + * @returns \OCA\Calendar\Db\CalendarCollection + * @throws DoesNotExistException if uri does not exist + * + * This method returns an \OCA\Calendar\Db\CalendarCollection object. + * This method is mandatory! + */ + public function findCalendars($userId, $limit=null, $offset=null) { + $collection = new CalendarCollection($this->findCalendar($this->calendarURI, $userId)); + $collection = $collection->subset($limit, $offset); + return $collection; + } + + /** + * @brief returns number of calendar + * @param string $userid + * @returns integer + * + * This method returns an integer + * This method is mandatory! + */ + public function countCalendars($userId) { + return 1; + } + + /** + * @brief returns whether or not a calendar exists + * @param string $calendarURI + * @param string $userid + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function doesCalendarExist($calendarURI, $userId) { + if($calendarURI === $this->calendarURI) { + return true; + } else { + return false; + } + } + + /** + * @brief returns information about the object (event/journal/todo) with the uid $objectURI in the calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $objectURI + * @param string $userid + * @returns \OCA\Calendar\Db\Object object + * @throws DoesNotExistException if calendar does not exist + * @throws DoesNotExistException if object does not exist + * + * This method returns an \OCA\Calendar\Db\Object object. + * This method is mandatory! + */ + public function findObject(Calendar &$calendar, $objectURI) { + //TODO implement + throw new DoesNotExistException(); + } + + /** + * @brief returns all objects in the calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $userId + * @returns \OCA\Calendar\Db\ObjectCollection + * @throws DoesNotExistException if calendar does not exist + * + * This method returns an \OCA\Calendar\Db\ObjectCollection object. + * This method is mandatory! + */ + public function findObjects(Calendar &$calendar, $limit, $offset) { + //TODO implement + return new ObjectCollection(); + } +} \ No newline at end of file diff --git a/backend/caldav.php b/backend/caldav.php new file mode 100644 index 000000000..d8bcc04a6 --- /dev/null +++ b/backend/caldav.php @@ -0,0 +1,16 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backend; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectType; + +use \OCA\Calendar\Db\Permissions; + +class CalDAV extends Backend {} \ No newline at end of file diff --git a/backend/ibackend.php b/backend/ibackend.php new file mode 100644 index 000000000..72e426758 --- /dev/null +++ b/backend/ibackend.php @@ -0,0 +1,218 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backend; + +use \OCA\Calendar\Db\Calendar; + +interface IBackend { + /** + * @brief get integer that represents supported actions + * @returns integer + * + * This method returns an integer. + * This method is mandatory! + */ + public function getSupportedActions(); + + /** + * @brief Check if backend implements actions + * @param string $actions + * @returns integer + * + * This method returns an integer. + * If the action is supported, it returns an integer that can be compared with \OC\Calendar\Backend\CREATE_CALENDAR, etc... + * If the action is not supported, it returns -501 + * This method is mandatory! + */ + public function implementsActions($actions); + + /** + * @brief returns whether or not a backend can be enabled + * @returns boolean + * + * This method returns a boolean. + * This method is mandatory! + */ + public function canBeEnabled(); + + /** + * @brief returns whether or not calendar objects should be cached + * @param string $calendarURI + * @param string $userId + * @returns boolean + * + * This method returns a boolen. + * This method is mandatory! + */ + public function cacheObjects($calendarURI, $userId); + + /** + * @brief returns information about calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $userId + * @returns array with \OCA\Calendar\Db\Calendar object + * @throws DoesNotExistException if uri does not exist + * + * This method returns an \OCA\Calendar\Db\Calendar object. + * This method is mandatory! + */ + public function findCalendar($calendarURI, $userId); + + /** + * @brief returns all calendars of the user $userId + * @param string $userId + * @returns \OCA\Calendar\Db\CalendarCollection + * @throws DoesNotExistException if uri does not exist + * + * This method returns an \OCA\Calendar\Db\CalendarCollection object. + * This method is mandatory! + */ + public function findCalendars($userId, $limit, $offset); + + /** + * @brief returns number of calendar + * @param string $userid + * @returns integer + * + * This method returns an integer + * This method is mandatory! + */ + public function countCalendars($userId); + + /** + * @brief returns whether or not a calendar exists + * @param string $calendarURI + * @param string $userid + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function doesCalendarExist($calendarURI, $userId); + + /** + * @brief returns ctag of a calendar + * @param string $calendarURI + * @param string $userid + * @returns integer + * @throws DoesNotExistException if calendar does not exist + * + * This method returns a integer + * This method is mandatory! + */ + public function getCalendarsCTag($calendarURI, $userId); + + /** + * @brief returns information about the object (event/journal/todo) with the uid $objectURI in the calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $objectURI + * @param string $userid + * @returns \OCA\Calendar\Db\Object object + * @throws DoesNotExistException if calendar does not exist + * @throws DoesNotExistException if object does not exist + * + * This method returns an \OCA\Calendar\Db\Object object. + * This method is mandatory! + */ + public function findObject(Calendar &$calendar, $objectURI); + + /** + * @brief returns all objects in the calendar $calendarURI of the user $userId + * @param string $calendarURI + * @param string $userId + * @returns \OCA\Calendar\Db\ObjectCollection + * @throws DoesNotExistException if calendar does not exist + * + * This method returns an \OCA\Calendar\Db\ObjectCollection object. + * This method is mandatory! + */ + public function findObjects(Calendar &$calendar, $limit, $offset); + + /** + * @brief returns number of objects in calendar + * @param string $calendarURI + * @param string $userid + * @returns integer + * @throws DoesNotExistException if calendar does not exist + * + * This method returns an integer + * This method is mandatory! + */ + public function countObjects(Calendar $calendar); + + /** + * @brief returns whether or not an object exists + * @param string $calendarURI + * @param string $objectURI + * @param string $userid + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function doesObjectExist(Calendar $calendar, $objectURI); + + /** + * @brief returns etag of an object + * @param string $calendarURI + * @param string $objectURI + * @param string $userid + * @returns string + * @throws DoesNotExistException if calendar does not exist + * @throws DoesNotExistException if object does not exist + * + * This method returns a string + * This method is mandatory! + */ + public function getObjectsETag(Calendar $calendar, $objectURI); + + /** + * @brief returns whether or not a backend can store a calendar's color + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreColor(); + + /** + * @brief returns whether or not a backend can store a calendar's supported components + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreComponents(); + + /** + * @brief returns whether or not a backend can store a calendar's displayname + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreDisplayname(); + + /** + * @brief returns whether or not a backend can store if a calendar is enabled + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreEnabled(); + + /** + * @brief returns whether or not a backend can store a calendar's order + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreOrder(); +} \ No newline at end of file diff --git a/backend/local.php b/backend/local.php new file mode 100644 index 000000000..75542ba82 --- /dev/null +++ b/backend/local.php @@ -0,0 +1,822 @@ + + * Copyright (c) 2014 Bart Visscher + * Copyright (c) 2014 Jakob Sack + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backend; + +use \OCP\AppFramework\IAppContainer; + +use \OCA\Calendar\Db\DoesNotExistException; +use \OCA\Calendar\Db\MultipleObjectsReturnedException; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\CalendarCollection; + +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectCollection; + +use \OCA\Calendar\Db\Timezone; +use \OCA\Calendar\Db\TimezoneCollection; + +use \OCA\Calendar\Db\ObjectType; +use \OCA\Calendar\Db\Permissions; + +use \DateTime; + +class Local extends Backend { + + private $calTableName; + private $objTableName; + + private $typeMapper = array( + ObjectType::EVENT => 'VEVENT', + ObjectType::JOURNAL => 'VJOURNAL', + ObjectType::TODO => 'VTODO', + ); + + private $reverseTypeMapper = array( + 'VEVENT' => ObjectType::EVENT, + 'VJOURNAL' => ObjectType::JOURNAL, + 'VTODO' => ObjectType::TODO, + ); + + public function __construct(IAppContainer $api, + array $parameters){ + + $this->calTableName = (array_key_exists('calTableName', $parameters) ? + $parameters['calTableName'] : + '*PREFIX*clndr_calendars'); + $this->objTableName = (array_key_exists('objTableName', $parameters) ? + $parameters['objTableName'] : + '*PREFIX*clndr_objects'); + + parent::__construct($api, 'local'); + } + + /** + * Shall calendars objects be cached? + * @param string $calendarURI + * @param string $userId + * @return boolean + */ + public function cacheObjects($calendarURI, $userId) { + return false; + } + + /** + * Find a calendar + * @param string $calendarURI + * @param string $userId + * @throws CacheOutDatedException if calendar does not exist + * @throws MultipleObjectsReturnedException if more than one result found + * @return Calendar + */ + public function findCalendar($calendarURI, $userId) { + $sql = 'SELECT * FROM `'. $this->calTableName . '` WHERE `uri` = ? AND `userid` = ?'; + $result = \OCP\DB::prepare($sql)->execute(array($calendarURI, $userId)); + $row = $result->fetchRow(); + + if($row === false || $row === null){ + $msg = 'Backend\Local::findCalendar(): Internal Error: '; + $msg .= 'No matching entry found'; + throw new CacheOutDatedException($msg); + } + + $row2 = $result->fetchRow(); + if(($row2 === false || $row2 === null ) === false) { + $msg = 'Backend\Local::findCalendar(): Internal Error: '; + $msg .= 'More than one result'; + throw new MultipleObjectsReturnedException($msg); + } + + $calendar = $this->createCalendarFromRow($row); + return $calendar; + } + + /** + * Find all calendars + * @param string $userId + * @param integer/null $limit + * @param integer/null $offset + * @return CalendarCollection + */ + public function findCalendars($userId, $limit, $offset) { + $sql = 'SELECT * FROM `' . $this->calTableName . '` WHERE `userid` = ?'; + $result = \OCP\DB::prepare($sql)->execute(array($userId)); //add limit offset thingy + + $calendarCollection = new CalendarCollection(); + while($row = $result->fetchRow()){ + try{ + $calendar = $this->createCalendarFromRow($row); + } catch(CorruptCalendarException $ex) { + //log error message + //if this happened, there is an corrupt entry + continue; + } + + $calendarCollection->add($calendar); + } + + return $calendarCollection; + } + + /** + * counts number of calendars + * @param string $userId + * @return integer + */ + public function countCalendars($userId) { + $sql = 'SELECT COUNT(*) FROM `' . $this->calTableName . '`'; + $sql .= ' WHERE `userid` = ?'; + + $result = \OCP\DB::prepare($sql)->execute(array( + $userId + )); + $count = $result->fetchOne(); + + if(gettype($count) !== 'integer') { + $count = intval($count); + } + + return $count; + } + + /** + * check if a calendar exists + * @param string $calendarURI + * @param string $userId + * @return boolean + */ + public function doesCalendarExist($calendarURI, $userId) { + $sql = 'SELECT COUNT(*) FROM `' . $this->calTableName . '`'; + $sql .= ' WHERE `uri` = ? AND `userid` = ?'; + + $result = \OCP\DB::prepare($sql)->execute(array( + $calendarURI, + $userId + )); + $count = $result->fetchOne(); + + if(gettype($count) !== 'integer') { + $count = intval($count); + } + + if($count === 0) { + return false; + } else { + return true; + } + } + + /** + * get ctag of a calendar + * @param string $calendarURI + * @param string $userId + * @return integer + */ + public function getCalendarsCTag($calendarURI, $userId) { + $sql = 'SELECT `ctag` FROM `' . $this->calTableName . '`'; + $sql .= ' WHERE `uri` = ? AND `userid` = ?'; + + $result = \OCP\DB::prepare($sql)->execute(array( + $calendarURI, + $userId + )); + $ctag = $result->fetchOne(); + + if(gettype($ctag) !== 'integer') { + $ctag = intval($ctag); + } + + return $ctag; + } + + /** + * Create a calendar + * @param Calendar $calendar + * @throws CacheOutDatedException if calendar already exists + * @return Calendar + */ + public function createCalendar(Calendar &$calendar) { + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + if($this->doesCalendarExist($calendarURI, $userId) === true) { + $msg = 'Backend\Local::createCalendar(): Internal Error: '; + $msg .= 'Calendar with uri and userid combination already exists!'; + throw new CacheOutDatedException($msg); + } + + $sql = 'INSERT INTO `' . $this->calTableName . '` '; + $sql .= '(`userid`, `displayname`, `uri`, `active`, `ctag`, `calendarorder`, '; + $sql .= '`calendarcolor`, `timezone`, `components`) '; + $sql .= 'VALUES(?,?,?,?,?,?,?,?,?)'; + $result = \OCP\DB::prepare($sql)->execute(array( + $calendar->getUserId(), + $calendar->getDisplayname(), + $calendar->getUri(), + $calendar->getEnabled(), + $calendar->getCtag(), + $calendar->getOrder(), + $calendar->getColor(), + $calendar->getTimezone(), + $calendar->getComponents(), + )); + + return $calendar; + } + + /** + * update a calendar + * @param Calendar $calendar + * @param string $oldCalendarURI + * @param string $oldUserId + * @throws CacheOutDatedException if calendar does not exist + * @return Calendar + */ + public function updateCalendar(Calendar &$calendar, $oldCalendarURI, $oldUserId) { + if($this->doesCalendarExist($oldCalendarURI, $oldUserId) === false) { + $msg = 'Backend\Local::updateCalendar(): Internal Error: '; + $msg .= 'Calendar with uri and userid combination not found!'; + throw new CacheOutDatedException($msg); + } + + $sql = 'UPDATE `' . $this->calTableName . '` SET '; + $sql .= '`userid` = ?, `displayname` = ?, `uri` = ?, `active` = ?, `ctag` = ?, '; + $sql .= '`calendarorder` = ?, `calendarcolor` = ?, `timezone` = ?, `components` = ? '; + $sql .= 'WHERE `userid` = ? AND `uri` = ?'; + $result = \OCP\DB::prepare($sql)->execute(array( + $calendar->getUserId(), + $calendar->getDisplayname(), + $calendar->getUri(), + $calendar->getEnabled(), + $calendar->getCtag(), + $calendar->getOrder(), + $calendar->getColor(), + $calendar->getTimezone(), + $calendar->getComponents(), + $oldUserId, + $oldCalendarURI, + )); + + return $calendar; + } + + /** + * delete a calendar + * @param string $calendarURI + * @param string $userId + * @return boolean + */ + public function deleteCalendar($calendarURI, $userId) { + $sql = 'DELETE FROM `' . $this->calTableName . '` '; + $sql .= '`uri` = ? AND `userid` = ?'; + $result = \OCP\DB::prepare($sql)->execute(array( + $calendarURI, + $userId + )); + + return $result; + } + + /** + * merge two calendars + * @param Calendar $calendar + * @param string $oldCalendarURI + * @param string $oldUserId + * @return boolean + */ + public function mergeCalendar(Calendar $calendar, $oldCalendarURI, $oldUserId) { + $newCalendarURI = $calendar->getUri(); + $newUserId = $calendar->getUserId(); + + //TODO - implement + + //$this->deleteCalendar($oldCalendarURI, $oldUserId); + } + + /** + * find object + * @param Calendar $calendar + * @param string $objectURI + * @throws CacheOutDatedException if calendar does not exist + * @throws MultipleObjectsReturnedException if more than one result found + * @return Object + */ + public function findObject(Calendar &$calendar, $objectURI) { + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + $sql = 'SELECT `' . $this->objTableName . '`.* FROM `' . $this->objTableName . '`, `' . $this->calTableName . '` '; + $sql .= 'WHERE `' . $this->objTableName . '`.`calendarid`=`' . $this->calTableName . '`.`id` '; + $sql .= 'AND `' . $this->calTableName . '`.`uri` = ? AND `' . $this->calTableName . '`.`userid` = ? '; + $sql .= 'AND `' . $this->objTableName . '`.`uri`= ?'; + $result = \OCP\DB::prepare($sql)->execute(array($calendarURI, $userId, $objectURI)); + $row = $result->fetchRow(); + + if($row === false || $row === null){ + $msg = 'Backend\Local::findObject(): Internal Error: '; + $msg .= 'No matching entry found'; + throw new CacheOutDatedException($msg); + } + + $row2 = $result->fetchRow(); + if(($row2 === false || $row2 === null ) === false) { + $msg = 'Backend\Local::findObject(): Internal Error: '; + $msg .= 'More than one result'; + throw new MultipleObjectsReturnedException($msg); + } + + $object = $this->createObjectFromRow($row, $calendar); + return $object; + } + + /** + * Find objecs + * @param Calendar $calendar + * @param integer/null $limit + * @param integer/null $offset + * @return ObjectCollection + */ + public function findObjects(Calendar &$calendar, $limit, $offset) { + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + $sql = 'SELECT `' . $this->objTableName . '`.* FROM `' . $this->objTableName . '`, `' . $this->calTableName . '` '; + $sql .= 'WHERE `' . $this->objTableName . '`.`calendarid`=`' . $this->calTableName . '`.`id` '; + $sql .= 'AND `' . $this->calTableName . '`.`uri` = ? AND `' . $this->calTableName . '`.`userid` = ?'; + $result = \OCP\DB::prepare($sql)->execute(array($calendarURI, $userId)); //add limit offset thingy + + $objectCollection = new ObjectCollection(); + while($row = $result->fetchRow()){ + try{ + $object = $this->createObjectFromRow($row, $calendar); + } catch(CorruptObjectException $ex) { + //log error message + //if this happened, there is an corrupt entry + continue; + } + + $objectCollection->add($object); + } + + return $objectCollection; + } + + /** + * Find objecs in period + * @param Calendar $calendar + * @param DateTime $start + * @param DateTime $end + * @param integer/null $limit + * @param integer/null $offset + * @return ObjectCollection + */ + public function findObjectsInPeriod(Calendar $calendar, DateTime $start, DateTime $end, $limit, $offset){ + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + $sql = 'SELECT `' . $this->objTableName . '`.* FROM `' . $this->objTableName . '`, `' . $this->calTableName . '` '; + $sql .= 'WHERE `' . $this->objTableName . '`.`calendarid`=`' . $this->calTableName . '`.`id`'; + $sql .= ' AND `' . $this->calTableName . '`.`uri` = ? AND `' . $this->calTableName . '`.`userid` = ?'; + $sql .= ' AND ((`' . $this->objTableName . '.startdate` >= ? AND '; + $sql .= '`' . $this->objTableName . '.enddate` <= ? AND `' . $this->objTableName . '.repeating` = 0)'; + $sql .= ' OR (`' . $this->objTableName . '.enddate` >= ? AND '; + $sql .= '`' . $this->objTableName . '.startdate` <= ? AND `' . $this->objTableName . '.repeating` = 0)'; + $sql .= ' OR (`' . $this->objTableName . '.startdate` <= ? AND `' . $this->objTableName . '.repeating` = 1))'; + + $start = $this->getUTCforMDB($start); + $end = $this->getUTCforMDB($end); + $result = $stmt->execute(array( + $calendarURI, $userId, + $start, $end, + $start, $end, + $end)); + + $objectCollection = new ObjectCollection(); + while($row = $result->fetchRow()){ + try{ + $object = $this->createObjectFromRow($row, $calendar); + } catch(CorruptObjectException $ex) { + //log error message + //if this happened, there is an corrupt entry + continue; + } + $objectCollection->add($object); + } + + return $objectCollection; + } + + /** + * Find objecs by type + * @param Calendar $calendar + * @param integer $type + * @param integer/null $limit + * @param integer/null $offset + * @return ObjectCollection + */ + public function findObjectsByType(Calendar $calendar, $type, $limit, $offset) { + $sql = 'SELECT `' . $this->objTableName . '`.* FROM `' . $this->objTableName . '`, `' . $this->calTableName . '` '; + $sql .= 'WHERE `' . $this->objTableName . '`.`calendarid`=`' . $this->calTableName . '`.`id` '; + $sql .= 'AND `' . $this->calTableName . '`.`uri` = ? and `' . $this->calTableName . '`.`userid` = ? AND `' . $this->objTableName . '`.`objecttype` = ?'; + $result = \OCP\DB::prepare($sql)->execute(array($calendarId, $userId, $type)); + + $objectCollection = new ObjectCollection(); + while($row = $result->fetchRow()){ + try{ + $object = $this->createObjectFromRow($row, $calendar); + } catch(CorruptObjectException $ex) { + //log error message + //if this happened, there is an corrupt entry + continue; + } + $objectCollection->add($object); + } + + return $objectCollection; + } + + /** + * Find objecs by type in period + * @param Calendar $calendar + * @param integer $type + * @param DateTime $start + * @param DateTime $end + * @param integer/null $limit + * @param integer/null $offset + * @return ObjectCollection + */ + public function findObjectsByTypeInPeriod(Calendar $calendar, $type, DateTime $start, DateTime $end, $limit, $offset) { + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + $sql = 'SELECT `' . $this->objTableName . '`.* FROM `' . $this->objTableName . '`, `' . $this->calTableName . '` '; + $sql .= 'WHERE `' . $this->objTableName . '`.`calendarid`=`' . $this->calTableName . '`.`id`'; + $sql .= ' AND `' . $this->calTableName . '`.`uri` = ? AND `' . $this->calTableName . '`.`userid` = ?'; + $sql .= ' AND ((`' . $this->objTableName . '.startdate` >= ? AND `' . $this->objTableName . '.enddate` <= ? AND `' . $this->objTableName . '.repeating` = 0)'; + $sql .= ' OR (`' . $this->objTableName . '.enddate` >= ? AND `' . $this->objTableName . '.startdate` <= ? AND `' . $this->objTableName . '.repeating` = 0)'; + $sql .= ' OR (`' . $this->objTableName . '.startdate` <= ? AND `' . $this->objTableName . '.repeating` = 1))'; + $sql .= ' AND `' . $this->objTableName . '.objecttype` = ?'; + + $start = $this->getUTCforMDB($start); + $end = $this->getUTCforMDB($end); + $result = $stmt->execute(array( + $calendarURI, $userId, + $start, $end, + $start, $end, + $end, + $type)); + + $objectCollection = new ObjectCollection(); + while($row = $result->fetchRow()){ + try{ + $object = $this->createObjectFromRow($row, $calendar); + } catch(CorruptObjectException $ex) { + //log error message + //if this happened, there is an corrupt entry + continue; + } + $objectCollection->add($object); + } + + return $objectCollection; + } + + /** + * count objects + * @param Calendar $calendar + * @return integer + */ + public function countObjects(Calendar $calendar) { + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + $sql = 'SELECT COUNT(*) FROM `' . $this->objTableName . '`, `' . $this->calTableName . '` '; + $sql .= 'WHERE `' . $this->objTableName . '`.`calendarid`=`' . $this->calTableName . '`.`id` '; + $sql .= 'AND `' . $this->calTableName . '`.`uri` = ? AND `' . $this->calTableName . '`.`userid` = ?'; + + //TODO validate if sql query is correct + + $result = \OCP\DB::prepare($sql)->execute(array( + $calendarURI, + $userId + )); + $count = $result->fetchOne(); + + if(gettype($count) !== 'integer') { + $count = intval($count); + } + + return $count; + } + + /** + * check if object exists + * @param Calendar $calendar + * @return boolean + */ + public function doesObjectExist(Calendar $calendar, $objectURI) { + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + $sql = 'SELECT COUNT(*) FROM `' . $this->objTableName . '`, `' . $this->calTableName . '` '; + $sql .= 'WHERE `' . $this->objTableName . '`.`calendarid`=`' . $this->calTableName . '`.`id` '; + $sql .= 'AND `' . $this->calTableName . '`.`uri` = ? AND `' . $this->calTableName . '`.`userid` = ?'; + + //TODO validate if sql query is correct + + $result = \OCP\DB::prepare($sql)->execute(array( + $calendarURI, + $userId + )); + $count = $result->fetchOne(); + + if(gettype($count) !== 'integer') { + $count = intval($count); + } + + if($count === 0) { + return false; + } else { + return true; + } + } + + /** + * get etag of an object + * @param Calendar $calendar + * @param string $objectURI + * @return integer + */ + public function getObjectsETag(Calendar $calendar, $objectURI) { + $object = $this->findObject($calendar, $objectURI); + return $object->generateEtag()->getEtag(); + } + + /** + * Create an object + * @param Object $object + * @throws CacheOutDatedException if calendar already exists + * @throws BackendException if object already exists + * @return Object + */ + public function createObject(Object &$object) { + $calendarURI = $object->calendar->getri(); + $userId = $object->calendar->getUserId(); + + if($this->doesCalendarExist($calendarURI, $userId) === true) { + $msg = 'Backend\Local::createObject(): Internal Error: '; + $msg .= 'Calendar with uri and userid combination already exists!'; + throw new CacheOutDatedException($msg); + } + + if($this->doesObjectExist($calendarURI, $userId) === true) { + $msg = 'Backend\Local::createObject(): User Error: '; + $msg .= 'Object already exists'; + throw new BackendException($msg); + } + + //TODO - update to fit new object structure + + $sql = 'INSERT INTO `' . $this->objTableName . '` '; + $sql .= '(`calendarid`,`objecttype`,`startdate`,`enddate`,`repeating`,`summary`,`calendardata`,`uri`,`lastmodified`) '; + $sql .= 'VALUES(?,?,?,?,?,?,?,?,?)'; + $result = \OCP\DB::prepare($sql)->execute(array( + $calendarDBId, + $object->getType(), + $object->getStartDate(), + $object->getEndDate(), + $object->getRepeating(), + $object->getSummary(), + $object->getCalendarData(), + $object->getObjectURI(), + $object->gerLastModified(), + )); + + return $object; + } + + /** + * update a calendar + * @param Object $object + * @param Calendar $oldCalendar + * @throws CacheOutDatedException if calendar does not exist + * @return Calendar + */ + public function updateObject(Object &$object, Calendar $oldCalendar) { + $calendarId = $object->getCalendarid(); + $userId = $object->getUserId(); + $calendarDBId = $this->getCalendarDBId($calendarId, $userId); + + $sql = 'INSERT INTO `' . $this->objTableName . '` '; + $sql .= '(`calendarid`,`objecttype`,`startdate`,`enddate`,`repeating`,`summary`,`calendardata`,`uri`,`lastmodified`) '; + $sql .= 'VALUES(?,?,?,?,?,?,?,?,?)'; + $result = \OCP\DB::prepare($sql)->execute(array( + $calendarDBId, + $object->getType(), + $object->getStartDate(), + $object->getEndDate(), + $object->getRepeating(), + $object->getSummary(), + $object->getCalendarData(), + $object->getObjectURI(), + $object->gerLastModified(), + )); + + return $object; + } + + public function deleteObject(Object $object){ + $userId = $object->getUserId(); + $calendarId = $object->getCalendarId(); + $objectURI = $object->getObjectURI(); + + $sql = 'DELETE * FROM `' . $this->objTableName . '`'; + $sql .= 'LEFT OUTER JOIN `' . $this->calTableName . '` ON '; + $sql .= '`' . $this->objTableName . '.calendarid`=`' . $this->calTableName . '.id`'; + $sql .= 'WHERE `' . $this->calTableName . '.uri` = ? AND `' . $this->calTableName . '.userid` = ?'; + $sql .= ' AND `' . $this->objTableName . '.uri` = ?'; + $result = \OCP\DB::prepare($sql)->execute(array( + $calendarId, $userId, + $objectURI)); + + return true; + } + + public function searchByProperties($properties=array(), $calendarId=null, $userId=null) { + if($calendarId === null || $userId === null) { + return array(); + } + + if(empty($properties)) { + return $this->findObjects($calendarId, $userId); + } + + $sql = 'SELECT `' . $this->objTableName . '`.* FROM `' . $this->objTableName . '`, `' . $this->calTableName . '` '; + $sql .= 'WHERE `' . $this->objTableName . '`.`calendarid`=`' . $this->calTableName . '`.`id` '; + $sql .= 'AND `' . $this->calTableName . '`.`uri` = ? AND `' . $this->calTableName . '`.`userid` = ?'; + + $parameters = array($calendarId, $userId); + $sqlWhereClauses = array(); + + foreach($properties as $key => $value) { + $key = strtoupper($key); + $value = strtolower($value); + + $sqlWhereClauses[] = 'WHERE UPPER(`' . $this->objTableName . '.calendardata`) LIKE `%?%`'; + $sqlWhereClauses[] = 'WHERE LOWER(`' . $this->objTableName . '.calendardata`) LIKE `%?%`'; + + $parameters[] = $key; + $parameters[] = $value; + + } + + $sql .= 'AND ('; + $sql .= implode(' AND ', $sqlWhereClauses); + $sql .= ')'; + + $result = \OCP\DB::prepare($sql)->execute($parameters); + + $objectCollection = array(); + while($row = $result->fetchRow()){ + $entity = new Object($row); + $this->completeObjectEntity($entity, $row); + $objectCollection->add($entity); + } + + return $objectCollection; + } + + private function getUTCforMDB($datetime){ + return date('Y-m-d H:i:s', $datetime->format('U')); + } + + private function getCalendarDBId($calendarId=null, $userId=null) { + if($calendarId === null || $userId === null) { + return null; + } + + $sql = 'SELECT id from `' . $this->calTableName . '` WHERE `uri` = ? AND `userid` = ?'; + $result = \OCP\DB::prepare($sql)->execute(array($calendarURI, $userId)); + + $calendarId = $result->fetchOne(); + return $calendarId; + } + + private function createCalendarFromRow(&$row) { + $calendar = new Calendar(); + + $calendar->setUserId((string) $row['userid']); + $calendar->setOwnerId((string) $row['userid']); + $calendar->setBackend((string) $this->backend); + $calendar->setUri((string) $row['uri']); + $calendar->setDisplayname((string) $row['displayname']); + $calendar->setComponents((int) ObjectType::getTypesByString($row['components'])); + $calendar->setCtag((int) $row['ctag']); + $calendar->setTimezone($this->createTimezoneFromRow($row)); + $calendar->setColor(($row['calendarcolor'] === null) ? '#ffffff' : (string) $row['calendarcolor']); + $calendar->setOrder((int) $row['calendarorder']); + $calendar->setEnabled((bool) $row['active']); + $calendar->setCruds(Permissions::ALL); + + $calendar->setComponents((int) $this->createComponentsFromRow($row)); + + if($calendar->isValid() !== true) { + //try to fix the calendar + $calendar->fix(); + + //check again + if($calendar->isValid() !== true) { + $msg = 'Backend\Local::createCalendarFromRow(): Internal Error: '; + $msg .= 'Received calendar data is not valid and not fixable! '; + $msg .= '(user:"' . $calendar->getUserId() . '"; '; + $msg .= 'calendar:"' . $calendar->getUri() . '")'; + throw new CorruptCalendarException($msg); + } + } + + return $calendar; + } + + private function createComponentsFromRow($row) { + return ObjectType::ALL; + } + + private function createTimezoneFromRow($row) { + return new Timezone(); + //return new Timezone($row['timezone'); + } + + private function createObjectFromRow(&$row) { + $object = new Object(); + $object->setObjectURI($row['uri']); + $object->setCalendarData($row['calendardata']); + + return $object; + } + + public function fetchCalendarPropertiesFromRemote() { + return true; + } + + /** + * @brief returns whether or not a backend can store a calendar's color + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreColor() { + return true; + } + + /** + * @brief returns whether or not a backend can store a calendar's supported components + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreComponents() { + return true; + } + + /** + * @brief returns whether or not a backend can store a calendar's displayname + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreDisplayname() { + return true; + } + + /** + * @brief returns whether or not a backend can store if a calendar is enabled + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreEnabled() { + return true; + } + + /** + * @brief returns whether or not a backend can store a calendar's order + * @returns boolean + * + * This method returns a boolean + * This method is mandatory! + */ + public function canStoreOrder() { + return true; + } +} \ No newline at end of file diff --git a/backend/localstorage.php b/backend/localstorage.php new file mode 100644 index 000000000..81f0ac984 --- /dev/null +++ b/backend/localstorage.php @@ -0,0 +1,16 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backend; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectType; + +use \OCA\Calendar\Db\Permissions; + +class LocalStorage extends Backend {} \ No newline at end of file diff --git a/backend/sharing.php b/backend/sharing.php new file mode 100644 index 000000000..dc8bf6132 --- /dev/null +++ b/backend/sharing.php @@ -0,0 +1,116 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + * + * naming schema: + * sharing-{sharingid} + * sharing-{sharingid}-{uid} + */ +namespace OCA\Calendar\Backend; + +use \OCA\Calendar\AppFramework\Core\API; +use \OCA\Calendar\AppFramework\Db\Mapper; +use \OCA\Calendar\AppFramework\Db\DoesNotExistException; +use \OCA\Calendar\AppFramework\Db\MultipleObjectsReturnedException; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectType; + +use \OCA\Calendar\Db\Permissions; + +class Sharing extends Backend { + + private $backend; + + private $crudsMapper = array( + \OCP\PERMISSION_CREATE => Permissions::CREATE, + \OCP\PERMISSION_READ => Permissions::READ, + \OCP\PERMISSION_UPDATE => Permissions::UPDATE, + \OCP\PERMISSION_DELETE => Permissions::DELETE, + \OCP\PERMISSION_SHARE => Permissions::SHARE, + \OCP\PERMISSION_ALL => Permissions::ALL, + ); + + private $reverseCrudsMapper = array( + Permissions::CREATE => \OCP\PERMISSION_CREATE, + Permissions::READ => \OCP\PERMISSION_READ, + Permissions::UPDATE => \OCP\PERMISSION_UPDATE, + Permissions::DELETE => \OCP\PERMISSION_DELETE, + Permissions::SHARE => \OCP\PERMISSION_SHARE, + Permissions::ALL => \OCP\PERMISSION_ALL, + ); + + public function __construct($api, $parameters, &$backendBusinessLayer){ + parent::__construct($api, 'sharing'); + $this->backend = $backendBusinessLayer; + } + + public function cacheObjects($calendarURI, $userId) { + return false; + } + + public function canBeEnabled() { + return \OCP\Share::isEnabled(); + } + + public function findCalendar($calendarURI, $userId) { + + } + + public function findCalendars($userId, $limit, $offset) { + $sharedCalendars = OCP\Share::getItemsSharedWith('calendar', OC_Share_Backend_Calendar::FORMAT_CALENDAR); + $singleSharedEvents = null; + } + + public function updateCalendar(Calendar $calendar, $calendarId, $userId) { + + } + + public function deleteCalendar(Calendar $calendar) { + + } + + public function mergeCalendar(Calendar $calendar, $calendarId=null, $userId=null) { + + } + + public function findObject($calendarURI, $objectURI, $userId) { + + } + + public function findObjects($calendarId, $userId, $limit, $offset) { + + } + + public function findObjectsInPeriod($calendarId, $start, $end, $userId, $limit, $offset){ + + } + + public function findObjectsByType($calendarId, $type, $userId, $limit, $offset) { + + } + + public function findObjectsByTypeInPeriod($calendarId, $type, $start, $end, $userId, $limit, $offset) { + + } + + public function createObject(Object $object, $userId) { + + } + + public function updateObject(Object $object, $calendarId, $uri, $userId) { + + } + + public function deleteObject(Object $object){ + + } + + public function searchByProperties($properties=array(), $calendarId=null, $userId=null) { + + } +} \ No newline at end of file diff --git a/backend/webcal.php b/backend/webcal.php new file mode 100644 index 000000000..74de289ab --- /dev/null +++ b/backend/webcal.php @@ -0,0 +1,27 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backend; + +use \OCA\Calendar\AppFramework\Core\API; +use \OCA\Calendar\AppFramework\Db\Mapper; +use \OCA\Calendar\AppFramework\Db\DoesNotExistException; +use \OCA\Calendar\AppFramework\Db\MultipleObjectsReturnedException; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectType; + +use \OCA\Calendar\Db\Permissions; + +class WebCal extends Backend { + + public function cacheObjects($calendarURI, $userId) { + return true; + } + +} \ No newline at end of file diff --git a/backgroundjob/task.php b/backgroundjob/task.php new file mode 100644 index 000000000..7fb3ddb66 --- /dev/null +++ b/backgroundjob/task.php @@ -0,0 +1,25 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Backgroundjob; + +use \OCA\Calendar\App; + +class Task { + static public function run() { + $app = new App(); + + + //TODO + //- [ ] update calendar cache from remote + //- [ ] make updater use limit and offset + //- [ ] cache repeating objects + //- [ ] make repeating objects cacher use limits and offset + + //$app->dispatch('Updater', 'update'); + } +} \ No newline at end of file diff --git a/businesslayer/businesslayer.php b/businesslayer/businesslayer.php new file mode 100644 index 000000000..314a65ace --- /dev/null +++ b/businesslayer/businesslayer.php @@ -0,0 +1,272 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\BusinessLayer; + +use \OCP\AppFramework\IAppContainer; + +use \OCA\Calendar\Db\Backend; +use \OCA\Calendar\Db\BackendCollection; +use \OCA\Calendar\Db\BackendMapper; + +use \OCA\Calendar\Db\DoesNotExistException; +use \OCA\Calendar\Db\MultipleObjectsReturnedException; + +use \OCA\Calendar\Utility\CalendarUtility; +use \OCA\Calendar\Utility\ObjectUtility; + +abstract class BusinessLayer { + + /** + * app container for dependency injection + * @var \OCP\AppFramework\IAppContainer + */ + protected $app; + + /** + * app container for dependency injection + * @var \OCA\Calendar\BusinessLayer\BackendBusinessLayer + */ + private $bmp; + + /** + * Backend Collection + * @var \OCA\Calendar\Db\BackendCollection + */ + protected $backends; + + /** + * Constructor + * @param IAppContainer $app interface to the app + * @param BackendBusinessLayer $backendBusinessLayer + */ + public function __construct(IAppContainer $app, BackendMapper $backendMapper){ + $this->app = $app; + $this->bmp = $backendMapper; + + $this->setupBackends(); + } + + /** + * get backend and uri from public uri + * public uri: backend-uri; e.g. local-work + * backend: backend; e.g. local for standard database backend + * real uri: uri; e.g. work + * @param string $uri public uri + * @throws BusinessLayerException if uri is empty + * @throws BusinessLayerException if uri is not valid + */ + final protected function splitCalendarURI($calendarId) { + $split = CalendarUtility::splitURI($calendarId); + + if($split[0] === false || $split[1] === false) { + throw new BusinessLayerException('calendar uri is not valid'); + } + + return $split; + } + + /** + * get backend and real calendar uri and real object uri from public uri + * public uri: backend-uri; e.g. local-work + * backend: backend; e.g. local for standard database backend + * real uri: uri; e.g. work + * @param string $uri public uri + * @throws BusinessLayerException if uri is empty + * @throws BusinessLayerException if uri is not valid + * @throws DoesNotImplementException if backend does not implement searched implementation + */ + final protected function splitObjectURI($objectURI=null) { + $split = ObjectUtility::splitURI($objectURI); + + if($split[0] === false || $split[1] === false || $split[2] === false) { + throw new BusinessLayerException('object uri is not valid'); + } + + return $split; + } + + /** + * check if a backend does support a certian action + * @param string $backend + * @param action + */ + final protected function doesBackendSupport($backend, $action) { + try { + return $this->backends->search('backend', $backend)->current()->api->implementsActions($action); + } catch(DoesNotExistException $ex){ + throw new BusinessLayerException($ex->getMessage()); + } catch(MultipleObjectsReturnedException $ex){ + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * check if a backend is enabled + * @param string $backend + */ + final protected function isBackendEnabled($backend) { + try { + return $this->backends->search('backend', $backend)->current()->getEnabled(); + } catch(DoesNotExistException $ex){ + throw new BusinessLayerException($ex->getMessage()); + } catch(MultipleObjectsReturnedException $ex){ + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * get the default backend + */ + final protected function getDefaultBackend(){ + return $this->bmp->getDefault(); + } + + /** + * set the default backend + * @param string $backend + */ + final protected function setDefaultBackend($backend){ + return $this->bmp->setDefault($backend); + } + + /** + * find a backend + * @param string $backend + */ + final protected function findBackend($backendId) { + try { + return $this->bmp->find($backendId); + } catch(DoesNotExistException $ex){ + throw new BusinessLayerException($ex->getMessage()); + } catch(MultipleObjectsReturnedException $ex){ + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * find all backend + * @param integer $limit + * @param integer $offset + */ + final protected function findAllBackends($limit=null, $offset=null) { + return $this->bmp->findAll($limit, $offset); + } + + /** + * find all disabled backend + * @param integer $limit + * @param integer $offset + */ + final protected function findAllDisabledBackeds($limit=null, $offset=null) { + return $this->bmp->findWhereEnabledIs(false, $limit, $offset); + } + + /** + * find all enabled backend + * @param integer $limit + * @param integer $offset + */ + final protected function findAllEnabledBackends($limit=null, $offset=null) { + return $this->bmp->findWhereEnabledIs(true, $limit, $offset); + } + + /** + * create a backend + * @param Backend $backend + */ + final protected function createBackend(Backend $backend) { + if($this->bmp->doesExist($backend)) { + $msg = 'BusinessLayer::allowNoBackendTwice(): '; + $msg .= 'Backend already exists'; + throw new BusinessLayerException($msg, BusinessLayerException::CONFLICT); + } + + return $this->bmp->create($create); + } + + /** + * update a backend + * @param Backend $backend + */ + final protected function updateBackend(Backend $backend) { + return $this->bmp->update($backend); + } + + /** + * enable a backend + * @param string $backend + */ + final protected function enableBackend($backendId){ + $backend = $this->findBackend($backendId)->setEnabled(false); + return $this->bmp->update($backend); + } + + /** + * disable a backend + * @param string $backend + */ + final protected function disableBackend($backendId){ + $backend = $this->findByName($backendId)->setEnabled(true); + return $this->bmp->update($backend); + } + + /** + * delete a backend + * @param Backend $backend + */ + final protected function deleteBackend(Backend $backend) { + return $this->bmp->delete($backend); + } + + /** + * setup backends + * @throws BusinessLayerException if no backends could be setup + */ + private function setupBackends() { + $backendCollection = new BackendCollection(); + + $enabledBackends = $this->findAllEnabledBackends(); + $enabledBackends->iterate(function($backend) use (&$backendCollection) { + $className = $backend->getClassname(); + $args = is_array($backend->getArguments()) ? $backend->getArguments() : array(); + + if(class_exists($className) === false){ + $msg = 'BusinessLayer::setupBackends(): '; + $msg .= '"' . $className . '" not found'; + \OCP\Util::writeLog('calendar', $msg, \OCP\Util::DEBUG); + $this->updateBackend($backend->disable()); + return false; + } + + if($backendCollection->search('backend', $backend->getBackend())->count() > 0) { + $msg = 'BusinessLayer::setupBackends(): '; + $msg .= '"' . $className . '" already initialized. '; + $msg .= 'Please check for double entries'; + \OCP\Util::writeLog('calendar', $msg, \OCP\Util::DEBUG); + return false; + } + + $reflectionObj = new \ReflectionClass($className); + $api = $reflectionObj->newInstanceArgs(array($this->app, $args, $this)); + $backend->registerAPI($api); + + //check if a backend can enabled + if($backend->api->canBeEnabled()) { + $backendCollection->add($backend); + } + }); + + if($backendCollection->count() === 0){ + $msg = 'BusinessLayer::setupBackends(): '; + $msg .= 'No backend was setup successfully'; + throw new BusinessLayerException($msg); + } + + $this->backends = $backendCollection; + } +} \ No newline at end of file diff --git a/businesslayer/businesslayerexception.php b/businesslayer/businesslayerexception.php new file mode 100644 index 000000000..ec3b22deb --- /dev/null +++ b/businesslayer/businesslayerexception.php @@ -0,0 +1,15 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\BusinessLayer; + +class BusinessLayerException extends \Exception { + const FORBIDDEN = 403; + const NOTFOUND = 404; + const CONFLICT = 409; + const INTERNAL = 500; +} \ No newline at end of file diff --git a/businesslayer/calendarbusinesslayer.php b/businesslayer/calendarbusinesslayer.php new file mode 100644 index 000000000..bfc98ac2d --- /dev/null +++ b/businesslayer/calendarbusinesslayer.php @@ -0,0 +1,740 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\BusinessLayer; + +use \OCP\AppFramework\IAppContainer; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\CalendarCollection; +use \OCA\Calendar\Db\CalendarMapper; + +use \OCA\Calendar\Db\ObjectType; + +use \OCA\Calendar\Db\BackendMapper; +use \OCA\Calendar\Db\DoesNotExistException; +use \OCA\Calendar\Db\MultipleObjectsReturnedException; + +use \OCA\Calendar\Utility\CalendarUtility; + +class CalendarBusinessLayer extends BusinessLayer { + + private $cmp; + private $obl; + + /** + * @param IAppContainer $app + * @param BackendMapper $backendMapper + * @param CalendarMapper $objectMapper: mapper for objects cache + * @param ObjectBusinessLayer $objectBusinessLayer + * @param API $api: an api wrapper instance + */ + public function __construct(IAppContainer $app, + BackendMapper $backendMapper, + CalendarMapper $calendarMapper, + ObjectBusinessLayer $objectBusinessLayer){ + + parent::__construct($app, $backendMapper); + + $this->cmp = $calendarMapper; + $this->obl = $objectBusinessLayer; + } + + + /** + * Find calendars of user $userId + * @param string $userId + * @param int $limit + * @param int $offset + * @throws BusinessLayerException + * @return CalendarCollection + */ + public function findAll($userId, $limit=null, $offset=null) { + try { + $calendars = $this->cmp->findAll($userId, $limit, $offset); + + //check if $calendars is a CalendarCollection, if not throw an exception + if(($calendars instanceof CalendarCollection) === false) { + $msg = 'CalendarBusinessLayer::findAll(): Internal Error: '; + $msg .= 'CalendarCache returned unrecognised format!'; + throw new BusinessLayerException($msg, BusinessLayerException::INTERNAL); + } + + $backends = $this->backends->enabled(); + $calendars = $calendars->filterByBackends($backends); + + return $calendars; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * number of calendars + * @param string $userId + * @throws BusinessLayerException + * @return integer + */ + public function numberOfCalendars($userId) { + try { + /*$number = $this->cmp->count($userId); + + //check if number is an integer, if not throw an exception + if(gettype($number) !== 'integer') { + $msg = 'CalendarBusinessLayer::numberOfAllCalendars(): Internal Error: '; + $msg .= 'CalendarCache returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + + //TODO filter for active backends only + + return $number;*/ + return $this->findAll($userId)->count(); + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * Find all calendars of user $userId on a backend + * @param string $backend + * @param string $userId + * @param int $limit + * @param int $offset + * @throws BusinessLayerException + * @return CalendarCollection + */ + public function findAllOnBackend($backend, $userId, $limit=null, $offset=null) { + try { + $calendars = $this->cmp->findAllOnBackend($backend, $userId, $limit, $offset); + + //check if $calendars is a CalendarCollection, if not throw an exception + if(($calendars instanceof CalendarCollection) === false) { + $msg = 'CalendarBusinessLayer::findAllOnBackend(): Internal Error: '; + $msg .= 'CalendarCache returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + + return $calendars; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * number of calendars on a backend + * @param string $backend + * @param string $userId + * @throws BusinessLayerException + * @return integer + */ + public function numberOfCalendarsOnBackend($backend, $userId) { + try { + $number = $this->cmp->countOnBackend($backend, $userId); + + //check if number is an integer, if not throw an exception + if(gettype($number) !== 'integer') { + $msg = 'CalendarBusinessLayer::numberOfAllCalendarsOnBackend(): Internal Error: '; + $msg .= 'CalendarCache returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + + return $number; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * Find calendar $calendarId of user $userId + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $userId + * @throws BusinessLayerException if backend does not exist + * @throws BusinessLayerException if backend is disabled + * @return calendar object + */ + public function find($calendarId, $userId) { + try { + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + if($this->isBackendEnabled($backend) !== true) { + $msg = 'CalendarBusinessLayer::find(): User Error: '; + $msg .= 'Backend found but not enabled'; + throw new BusinessLayerException($msg); + } + + $calendar = $this->cmp->find($backend, $calendarURI, $userId); + + if(($calendar instanceof Calendar) === false) { + $msg = 'CalendarBusinessLayer::find(): Internal Error: '; + $msg .= 'CalendarCache returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + + return $calendar; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (MultipleObjectsReturnedException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * checks if a calendar exists + * @param string $calendarId + * @param string $userId + * @param boolean $checkRemote + * @return boolean + */ + public function doesExist($calendarId, $userId, $checkRemote=false) { + try { + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + $doesExistCached = $this->cmp->doesExist($backend, $calendarURI, $userId); + + if($checkRemote === false) { + return $doesExistCached; + } + + $doesExistRemote = $this->backends->find($backend)->api->doesCalendarExist($calendarURI, $userId); + + if($doesExistCached !== $doesExistRemote) { + $this->updateCacheForCalendarFromRemote(array($backend, $calendarURI), $userId); + } + + return $doesExistRemote; + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * checks if a calendar allows a certain action + * @param int $cruds + * @param string $calendarId + * @param string $userId + * @return boolean + */ + public function doesAllow($cruds, $calendarId, $userId) { + try { + if(is_array($calendarId)) { + $backend = $oldCalendarId[0]; + $calendarURI = $oldCalendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + return $this->cmp->doesAllow($cruds, $backend, $calendarURI, $userId); + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * checks if a calendar supports a certian component + * @param int $component + * @param string $calendarId + * @param string $userId + * @return boolean + */ + public function doesSupport($component, $calendarId, $userId) { + try { + if(is_array($calendarId)) { + $backend = $oldCalendarId[0]; + $calendarURI = $oldCalendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + return $this->cmp->doesSupport($component, $backend, $calendarURI, $userId); + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * create a new calendar + * @param Calendar $calendar + * @throws BusinessLayerException if name exists already + * @throws BusinessLayerException if backend does not exist + * @throws BusinessLayerException if backend is disabled + * @throws BusinessLayerException if backend does not implement creating a calendar + * @return Calendar $calendar - calendar object + */ + public function create(Calendar $calendar) { + try { + $backend = $calendar->getBackend(); + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + if(!$calendar->isValid()) { + $msg = 'CalendarBusinessLayer::create(): User Error: '; + $msg .= 'Given calendar data is not valid!'; + throw new BusinessLayerException($msg); + } + + if(!$this->isBackendEnabled($backend)) { + $msg = 'CalendarBusinessLayer::create(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + if($this->doesCalendarExist(array($backend, $calendarURI), $userId)) { + $msg = 'CalendarBusinessLayer::create(): User Error: '; + $msg .= 'Calendar already exists!'; + throw new BusinessLayerException($msg); + } + if(!$this->doesBackendSupport($backend, \OCA\Calendar\Backend\CREATE_CALENDAR)) { + $msg = 'CalendarBusinessLayer::create(): User Error: '; + $msg .= 'Backend does not support creating calendars!'; + throw new BusinessLayerException($msg); + } + + $this->backends->find($backend)->api->createCalendar($calendar); + $this->mapper->insert($calendar); + + return $calendar; + } catch (DoesNotImplementException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (CacheOutDatedException $ex) { + $this->updateCacheForCalendarFromRemote(array($backend, $calendarURI), $userId); + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * update a new calendar + * @param Calendar $calendar + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $userId + * @throws BusinessLayerException if backend does not exist + * @throws BusinessLayerException if backend is disabled + * @throws BusinessLayerException if backend does not implement updating a calendar + * @return Calendar $calendar - calendar object + */ + public function update(Calendar $calendar, $oldCalendarId, $oldUserId) { + try { + if(is_array($oldCalendarId)) { + $oldBackend = $oldCalendarId[0]; + $oldCalendarURI = $oldCalendarId[1]; + } else { + list($oldBackend, $oldCalendarURI) = $this->splitCalendarURI($oldCalendarId); + } + + if($calendar->doesContainNullValues()) { + $oldCalendarObject = $this->find(array($oldBackend, $oldCalendarURI), $oldUserId); + $oldCalendarObject->overwriteWith($calendar); + $calendar = $oldCalendarObject; + } + + $newBackend = $calendar->getBackend(); + $newCalendarURI = $calendar->getUri(); + $newUserId = $calendar->getUserId(); + + if($oldUserId !== $newUserId) { + $msg = 'CalendarBusinessLayer::update(): Not supported: '; + $msg .= 'Transferring a calendar to another user is not supported yet.'; + throw new BusinessLayerException($msg); + } + if($this->isBackendEnabled($oldBackend) !== true) { + $msg = 'CalendarBusinessLayer::update(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + if($newBackend !== $oldBackend && $this->isBackendEnabled($newBackend) !== true) { + $msg = 'CalendarBusinessLayer::update(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + + $oldBackendsAPI = &$this->backends->find($oldBackend)->api; + $newBackendsAPI = &$this->backends->find($newBackend)->api; + + if($calendar->isValid() !== true) { + $msg = 'CalendarBusinessLayer::update(): User Error: '; + $msg .= 'Given calendar data is not valid!'; + throw new BusinessLayerException($msg); + } + + /* Move calendar to another backend when: + * - [x] the backend changed + * - [x] uri is available on the other calendar + */ + if($newBackend !== $oldBackend && !$this->doesCalendarExist(array($newBackend, $newCalendarURI), $newUserId)) { + if(!$this->doesBackendSupport($oldBackend, \OCA\Calendar\Backend\DELETE_CALENDAR)) { + $msg = 'CalendarBusinessLayer::update(): User Error: '; + $msg .= 'Backend does not support deleting calendars!'; + throw new BusinessLayerException($msg); + } + if(!$this->doesBackendSupport($newBackend, \OCA\Calendar\Backend\CREATE_CALENDAR)) { + $msg = 'CalendarBusinessLayer::update(): User Error: '; + $msg .= 'Backend does not support creating calendars!'; + throw new BusinessLayerException($msg); + } + + //create calendar on new backend + $calendar = $newBackendsAPI->createCalendar($calendar); + + //move all objects + $this->obl->moveAll($calendar, array($oldBackend, $oldCalendarURI), $newUserId); + + //if no exception was thrown, + //moving objects finished without any problem + $oldBackendsAPI->deleteCalendar($oldCalendarURI, $userId); + $this->cmp->update($calendar); + $this->updateFromCache(array($oldBackend, $oldCalendarURI), $oldUserId); + + return $calendar; + } else + /* Merge calendar with another one when: + * - [x] the backend changed + * - [x] uri is not available on the other backend + * or: + * - [x] backend didn't change + * - [x] uri changed + * - [x] uri is not available + */ + if(($newBackend !== $oldBackend || $newCalendarURI !== $oldCalendarURI) && $this->doesCalendarExist(array($newBackend, $newCalendarURI), $newUserId)) { + if($newBackend === $oldBackend && $this->doesBackendSupport($oldBackend, \OCA\Calendar\Backend\MERGE_CALENDAR)) { + $newBackendsAPI->mergeCalendar($calendar, $oldCalendarURI, $oldUserId); + } else { + if(!$this->doesBackendSupport($oldBackend, \OCA\Calendar\Backend\DELETE_CALENDAR)) { + $msg = 'CalendarBusinessLayer::update(): User Error: '; + $msg .= 'Backend does not support deleting calendars!'; + throw new BusinessLayerException($msg); + } + + //move all objects + $this->obl->moveAll($calendar, $oldCalendarId, $oldUserId); + + //if no exception was thrown, + //moving objects finished without any problem + $this->cmp->update($calendar); + $oldBackendsAPI->deleteCalendar($oldCalendarURI, $oldUserId); + $this->updateFromCache(array($oldBackend, $oldCalendarURI), $oldUserId); + } + + return $calendar; + } else { + /* Update the calendar when: + * - [x] the backend didn't change + * - [x] the uri didn't change + */ + if($this->doesBackendSupport($backend, \OCA\Calendar\Backend\UPDATE_CALENDAR) === true) { + $newBackendsAPI->updateCalendar($calendar, $oldCalendarURI); + } + $this->mapper->updateEntity($calendar); + return $calendar; + } + } catch(DoesNotImplementException $ex) { + //write debug note to logfile + throw new BusinessLayerException($ex->getMessage()); + } catch(BackendException $ex) { + //write debug note to logfile + throw new BusinessLayerException($ex->getMessage()); + } catch (CalendarNotFixable $ex) { + //write error note to logfile + throw new BusinessLayerException($ex->getMessage()); + } catch (CacheOutDatedException $ex) { + //write debug note to logfile + $this->updateCacheForCalendarFromRemote($oldCalendarId, $userId); + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * touch a calendar aka increment a calendars ctag + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $userId + * @throws BusinessLayerException if backends do not exist + * @throws BusinessLayerException if backends are disabled + * @throws BusinessLayerException if backend does not implement updating a calendar + * @return Calendar $calendar - calendar object + */ + public function touch($calendarId, $userId) { + try { + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + $calendar = $this->find(array($backend, $calendarURI), $userId); + $calendar->touch(); + $calendar = $this->update($calendar, array($backend, $calendarURI), $userId); + + return $calendar; + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * delete a calendar + * @param Calendar $calendar + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $userId + * @throws BusinessLayerException if backend does not exist + * @throws BusinessLayerException if backend is disabled + * @throws BusinessLayerException if backend does not implement updating a calendar + */ + public function delete($calendarId, $userId) { + try { + if(is_array($calendarId)) { + $backend = $oldCalendarId[0]; + $calendarURI = $oldCalendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + $calendar = $this->find(array($backend, $calendarURI), $userId); + + if(!$this->isBackendEnabled($backend)) { + $msg = 'CalendarBusinessLayer::delete(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + if(!$this->doesBackendSupport($backend, \OCA\Calendar\Backend\DELETE_CALENDAR)) { + $msg = 'CalendarBusinessLayer::delete(): User Error: '; + $msg .= 'Backend does not support deleting calendars!'; + throw new BusinessLayerException($msg); + } + + $this->backends->find($backend)->api->deleteCalendar($calendarURI, $userId); + $this->cmp->delete($calendar); + } catch(DoesNotImplementException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + + /** + * checks if a calendar exists + * @param string $calendarId + * @param string $userId + * @return boolean + */ + public function isCalendarOutDated($calendarId, $userId=null) { + try{ + if(is_array($calendarId)) { + $backend = $oldCalendarId[0]; + $calendarURI = $oldCalendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + $cachedCtag = $cachedCalendar->getCtag(); + $remoteCtag = $remoteCalendar->getCtag(); + if($cachedCtag === $remoteCtag) { + return false; + } + if($cachedCtag < $remoteCtag) { + return true; + } + if($cachedCtag > $remoteCalendar) { + //TODO - how to handle this case appropriately? + //could lead to endless updates if backend is sending broken ctag + //setting cached ctag to remote ctag will break client sync + } + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(DoesNotExistException $ex) { + + } + } + + /** + * update all calendars of a user + * @param string $userId + * @return boolean + */ + public function updateCacheForAllFromRemote($userId) { + try{ + $backends = $this->backends->getObjects(); + + foreach($backends as $backend) { + try{ + $backendName = $backend->getBackend(); + $this->updateCacheForBackendFromRemote($backendName, $userId); + } catch(BusinessLayerException $ex) { + //TODO - log error msg + continue; + } + } + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * update all calendars of a user on a backend + * @param string $backend + * @param string $userId + * @return boolean + */ + public function updateCacheForBackendFromRemote($backend, $userId) { + try{ + $calendars = $this->findAllOnBackend($backend, $userId); + + $remoteCalendars = $this->backends->find($backend)->api->findCalendars($userId); + + $calendars->addCollection($remoteCalendars)->noDuplicates(); + + foreach($calendars->getObjects() as $calendar) { + try{ + $backend = $calendar->getBackend(); + $calendarURI = $calendar->getUri(); + + $this->updateCacheForCalendarFromRemote(array($backend, $calendarURI), $userId); + } catch(BusinessLayerException $ex) { + //TODO - log error msg + continue; + } catch(DoesNotExistException $ex) { + //should not occur, but catch it nevertheless + continue; + } + } + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * update a specific calendar + * @param string $userId + * @return boolean + */ + public function updateCacheForCalendarFromRemote($calendarId, $userId) { + try{ + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + $remoteAPI = &$this->backends->find($backend)->api; + + $doesCalendarExistCached = $this->doesExist(array($backend, $calendarURI), $userId, false); + $doesCalendarExistRemote = $remoteAPI->doesCalendarExist($calendarURI, $userId); + + if($doesCalendarExistCached === false && $doesCalendarExistRemote === false) { + $msg = 'CalendarBusinessLayer::updateCacheForCalendarFromRemote(): '; + $msg .= '"b:' . $backend . ';u:' . $calendarURI . '" doesn\'t exist'; + $msg .= 'Neither cached nor remote!'; + throw new DoesNotExistException($msg); + } + + if($doesCalendarExistRemote === false) { + $msg = 'CalendarBusinessLayer::updateCacheForCalendarFromRemote(): '; + $msg .= 'Calendar vanished from remote - removing calendar from cache!'; + //TODO - log debug message + + $cachedCalendar = $this->find(array($backend, $calendarURI), $userId); + + $this->obl->deleteAll(array($backend, $calendarURI), $userId); + $this->cmp->delete($cachedCalendar); + return true; + } + + $remoteCalendar = $remoteAPI->findCalendar($calendarURI, $userId); + + if($doesCalendarExistCached === false) { + $msg = 'CalendarBusinessLayer::updateCacheForCalendarFromRemote(): '; + $msg .= 'Calendar not cached - creating calendar from remote!'; + //TODO - log debug message + + if($remoteCalendar->isValid() !== true) { + $msg = 'CalendarBusinessLayer::updateCacheForCalendarFromRemote(): Backend Error: '; + $msg .= 'Given calendar data is not valid!'; + throw new BusinessLayerException($msg); + } + + $this->cmp->insert($remoteCalendar); + $this->obl->updateCacheForCalendarFromRemote(array($backend, $calendarURI), $userId); + return true; + } + + $cachedCalendar = $this->find(array($backend, $calendarURI), $userId); + + if($cachedCalendar == $remoteCalendar) { + return true; + } + + if($cachedCalendar->getCtag() < $remoteCalendar->getCtag()) { + $this->obl->updateCacheForCalendarFromRemote(array($backend, $calendarURI), $userId); + } + + if($remoteAPI->canStoreColor() === false) { + $remoteCalendar->setColor(null); + } + if($remoteAPI->canStoreComponents() === false) { + $remoteCalendar->setComponents(null); + } + if($remoteAPI->canStoreDisplayname() === false) { + $remoteCalendar->setDisplayname(null); + } + if($remoteAPI->canStoreEnabled() === false) { + $remoteCalendar->setEnabled(null); + } + if($remoteAPI->canStoreOrder() === false) { + $remoteCalendar->setOrder(null); + } + + $cachedCalendar->overwriteWith($remoteCalendar); + + if($cachedCalendar->isValid() !== true) { + $msg = 'CalendarBusinessLayer::updateCacheForCalendarFromRemote(): Backend Error: '; + $msg .= 'Given calendar data is not valid!'; + throw new BusinessLayerException($msg); + } + + $this->cmp->update($cachedCalendar); + + return true; + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } +} \ No newline at end of file diff --git a/businesslayer/objectbusinesslayer.php b/businesslayer/objectbusinesslayer.php new file mode 100644 index 000000000..d89938c2c --- /dev/null +++ b/businesslayer/objectbusinesslayer.php @@ -0,0 +1,788 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\BusinessLayer; + +use \OCP\AppFramework\IAppContainer; + +use \OCA\Calendar\Db\DoesNotExistException; +use \OCA\Calendar\Db\MultipleObjectsReturnedException; + +use \OCA\Calendar\Db\BackendMapper; +use \OCA\Calendar\Db\ObjectMapper; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\CalendarCollection; + +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectCollection; + +use \OCA\Calendar\Db\Timezone; +use \OCA\Calendar\Db\TimezoneCollection; + +use \OCA\Calendar\Backend\BackendException; +use \OCA\Calendar\Backend\DoesNotImplementException; + +class ObjectBusinessLayer extends BusinessLayer { + + private $omp; + + private $runTimeCache=array(); + private $remoteObjectObjectCache=array(); + + /** + * @param ObjectMapper $objectMapper: mapper for objects cache + * @param CalendarBusinessLayer $calendarBusinessLayer + * @param BackendBusinessLayer $backendBusinessLayer + * @param API $api: an api wrapper instance return new ObjectBusinessLayer($c, $bbl, $omp); + */ + public function __construct(IAppContainer $app, + BackendMapper $backendMapper, + ObjectMapper $objectMapper){ + parent::__construct($app, $backendMapper); + $this->omp = $objectMapper; + } + + + /** + * Finds all objects of calendar $calendarId of user $userId + * @param Calendar $calendar; + * @param int limit + * @param int offset + * @throws BusinessLayerException + * @return ObjectCollection + */ + public function findAll(Calendar &$calendar, $limit=null, $offset=null) { + try { + $backend = $calendar->getBackend(); + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + if($this->isBackendEnabled($backend) !== true) { + $msg = 'ObjectBusinessLayer::findAll(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg, BusinessLayerException::INTERNAL); + } + + $api = &$this->backends->find($backend)->api; + + $cacheObjects = $api->cacheObjects($calendarURI, $userId); + if($cacheObjects === true) { + $objects = $this->omp->findAll($calendar, $limit, $offset); + } else { + $objects = $api->findObjects($calendar, $limit, $offset); + } + + //check if $calendars is a CalendarCollection, if not throw an exception + if(($objects instanceof ObjectCollection) === false) { + $msg = 'ObjectBusinessLayer::findAll(): Internal Error: '; + $msg .= ($cacheObjects ? 'ObjectCache' : 'Backend') . ' returned unrecognised format!'; + throw new BusinessLayerException($msg, BusinessLayerException::INTERNAL); + } + + return $objects; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * get the number how many calendars a user has + * @param Calendar &$calendar Calendar object + * @throws BusinessLayerException + * @return integer + */ + public function numberOfObjects(Calendar &$calendar) { + try { + $backend = $calendar->getBackend(); + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + if($this->isBackendEnabled($backend) !== true) { + $msg = 'ObjectBusinessLayer::findAll(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + + $api = &$this->backends->find($backend)->api; + + $cacheObjects = $api->cacheObjects($calendarURI, $userId); + //TODO implement + if($cacheObjects === true) { + $number = 0; + } else { + $number = 0; + } + + //check if number is an integer, if not throw an exception + if(gettype($number) !== 'integer') { + $msg = 'CalendarBusinessLayer::numberOfObjects(): Internal Error: '; + $msg .= ($cacheObjects ? 'ObjectCache' : 'Backend') . ' returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + + return $number; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * Find the object $objectURI of calendar $calendarId of user $userId + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $objectURI UID of the object + * @throws BusinessLayerException + * @return object + */ + public function find(Calendar &$calendar, $objectURI) { + try { + $backend = $calendar->getBackend(); + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + if($this->isBackendEnabled($backend) !== true) { + $msg = 'ObjectBusinessLayer::find(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + + $api = &$this->backends->find($backend)->api; + + $cacheObjects = $api->cacheObjects($calendarURI, $userId); + if($cacheObjects === true) { + $object = $this->omp->find($calendar, $objectURI); + } else { + $object = $api->findObject($calendar, $objectURI); + } + + if(($object instanceof Object) === false) { + $msg = 'ObjectBusinessLayer::find(): Internal Error: '; + $msg .= ($cacheObjects ? 'ObjectCache' : 'Backend') . ' returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + + return $object; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (MultipleObjectsReturnedException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * Find the object $objectURI of type $type of calendar $calendarId of user $userId + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $objectURI UID of the object + * @param string $type type of the searched objects, use OCA\Calendar\Db\ObjectType + * @param string $userId + * @throws BusinessLayerException + * @return object + */ + public function findByType(Calendar &$calendar, $objectURI, $type) { + try { + $object = $this->find($calendar, $objectURI); + + if($object->getType() !== $type) { + $msg = 'ObjectBusinessLayer::find(): User Error: '; + $msg .= 'Requested object exists but is of different type!'; + throw new BusinessLayerException($msg); + } + + return $object; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (MultipleObjectsReturnedException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * Finds all objects of type $type of calendar $calendarId of user $userId + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $type type of the searched objects, use OCA\Calendar\Db\ObjectType + * @param string $userId + * @param int limit + * @param int offset + * @throws BusinessLayerException + * @return array containing all items + */ + public function findAllByType(Calendar &$calendar, $type, $limit=null, $offset=null) { + try { + $backend = $calendar->getBackend(); + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + if($this->isBackendEnabled($backend) !== true) { + $msg = 'ObjectBusinessLayer::findAllByType(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + + $cacheObjects = $api->cacheObjects($calendarURI, $userId); + if($cacheObjects === true) { + $objects = $this->omp->findAllByType($calendar, $type, $limit, $offset); + } else { + $api = &$this->backends->find($backend)->api; + + $doesBackendSupport = $this->doesBackendSupport($backend, \OCA\Calendar\Backend\FIND_OBJECTS_BY_TYPE); + if($doesBackendSupport === true) { + $objects = $api->findObjectsByType($calendar, $type, $limit, $offset); + } else { + //TODO - don't query for all + //query for subsets until we've got what we want + $objects = $api->findObjects($calendar); + if($objects instanceof ObjectCollection) { + $objects = $objects->byType($type)->subset($limit, $offset); + } + } + } + + //check if $objects is a ObjectCollection, if not throw an exception + if(($objects instanceof ObjectCollection) === false) { + $msg = 'ObjectBusinessLayer::findAllByType(): Internal Error: '; + $msg .= ($cacheObjects ? 'ObjectCache' : 'Backend') . ' returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + + return $objects; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * Finds all objects in timespan from $start to $end of calendar $calendarId of user $userId + * @param string $calendarId global uri of calendar e.g. local-work + * @param DateTime $start start of timeframe + * @param DateTime $end end of timeframe + * @param string $userId + * @param int limit + * @param int offset + * @throws BusinessLayerException + * @return array containing all items + */ + public function findAllInPeriod(Calendar &$calendar, $start, $end, $limit=null, $offset=null) { + try { + $backend = $calendar->getBackend(); + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + if($this->isBackendEnabled($backend) !== true) { + $msg = 'ObjectBusinessLayer::findAllInPeriod(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + + $cacheObjects = $api->cacheObjects($calendarURI, $userId); + if($cacheObjects === true) { + $objects = $this->omp->findAllInPeriod($calendar, $start, $end, $limit, $offset); + + //check if $objects is a ObjectCollection, if not throw an exception + if(($objects instanceof ObjectCollection) === false) { + $msg = 'ObjectBusinessLayer::findAllInPeriod(): Internal Error: '; + $msg .= 'ObjectCache returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + } else { + $api = &$this->backends->find($backend)->api; + + $doesBackendSupport = $this->doesBackendSupport($backend, \OCA\Calendar\Backend\FIND_IN_PERIOD); + if($doesBackendSupport === true) { + $objects = $api->findObjectsInPeriod($calendar, $type, $limit, $offset); + } else { + $objects = $api->findObjects($calendar); + } + + //check if $objects is a ObjectCollection, if not throw an exception + if(($objects instanceof ObjectCollection) === false) { + $msg = 'ObjectBusinessLayer::findAllByType(): Internal Error: '; + $msg .= 'Backend returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + + if($doesBackendSupport === false) { + $objects = $objects->inPeriod($start, $end)->subset($limit, $offset); + } + } + + return $objects; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * Finds all objects of type $type in timeframe from $start to $end of calendar $calendarId of user $userId + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $type type of the searched objects, use OCA\Calendar\Db\ObjectType + * @param DateTime $start start of the timeframe + * @param DateTime $end end of the timeframe + * @param string $userId + * @param boolean $expand expand if repeating event + * @param DateTime $expandStart don't return repeating events earlier than $expandStart + * @param DateTime $expandEnd don't return repeating events later than $expandEnd + * @param int limit + * @param int offset + * @throws BusinessLayerException + * @return array containing all items + */ + public function findAllByTypeInPeriod(Calendar &$calendar, $type, $start, $end, $limit=null, $offset=null) { + try { + $backend = $calendar->getBackend(); + $calendarURI = $calendar->getUri(); + $userId = $calendar->getUserId(); + + if($this->isBackendEnabled($backend) !== true) { + $msg = 'ObjectBusinessLayer::findAllInPeriod(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + + $cacheObjects = $api->cacheObjects($calendarURI, $userId); + if($cacheObjects === true) { + $objects = $this->omp->findAllByTypeInPeriod($calendar, $start, $end, $type); + + //check if $objects is a ObjectCollection, if not throw an exception + if(($objects instanceof ObjectCollection) === false) { + $msg = 'ObjectBusinessLayer::findAllByTypeInPeriod(): Internal Error: '; + $msg .= 'ObjectCache returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + } else { + $api = &$this->backends->find($backend)->api; + + $doesBackendSupport = $this->doesBackendSupport($backend, \OCA\Calendar\Backend\FIND_IN_PERIOD_BY_TYPE); + if($doesBackendSupport === true) { + $objects = $api->findObjectsByTypeInPeriod($calendar, $start, $end, $type); + } else { + $objects = $api->findObjects($calendar); + } + + //check if $objects is a ObjectCollection, if not throw an exception + if(($objects instanceof ObjectCollection) === false) { + $msg = 'ObjectBusinessLayer::findAllByType(): Internal Error: '; + $msg .= 'Backend returned unrecognised format!'; + throw new BusinessLayerException($msg); + } + + if($doesBackendSupport === false) { + $objects = $objects->byType($type)->inPeriod($start, $end)->subset($limit, $offset); + } + } + + return $objects; + } catch (DoesNotExistException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch (BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * creates a new object + * @param Object $object + * @throws BusinessLayerException + * @return array containing all items + */ + public function create(Object $object) { + try { + $backend = $object->calendar->getBackend(); + $calendarURI = $object->calendar->getUri(); + $objectURI = $object->getObjectURI(); + + if($this->isBackendEnabled($backend) !== true) { + $msg = 'ObjectBusinessLayer::create(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + //validate that calendar exists + if($this->doesObjectExist($calendarId, $objectURI, $userId) !== false) { + $msg = 'ObjectBusinessLayer::create(): User Error: '; + $msg .= 'Object already exists!'; + throw new BusinessLayerException($msg); + } + if($this->doesBackendSupport($backend, \OCA\Calendar\Backend\CREATE_OBJECT) !== true) { + $msg = 'ObjectBusinessLayer::create(): User Error: '; + $msg .= 'Backend does not support creating objects!'; + throw new BusinessLayerException($msg); + } + + if($object->isValid() !== true) { + //try to fix the object + $object->fix(); + + //check again + if($object->isValid() !== true) { + $msg = 'ObjectBusinessLayer::create(): User Error: '; + $msg .= 'Given object data is not valid and not fixable'; + throw new BusinessLayerException($msg); + } + } + + $api->createObject($object, $calendarURI, $objectURI, $userId); + + $cacheObjects = $api->cacheObjects($calendarURI, $userId); + if($cacheObjects === true) { + $this->omp->insert($object, $calendarURI, $objectURI, $userId); + } + + $this->calendarBusinessLayer->touch($calendarId, $userId); + + return $object; + } catch(DoesNotImplementException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * updates an object + * @param Object $object + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $objectURI UID of the object + * @param string $userId + * @throws BusinessLayerException + * @return array containing all items + */ + public function update(Object $object, Calendar $calendar, $objectURI, $userId) { + try { + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + if($this->isBackendEnabled($backend) !== true) { + $msg = 'ObjectBusinessLayer::findAllByType(): User Error: '; + $msg .= 'Backend found but not enabled!'; + throw new BusinessLayerException($msg); + } + + if($object->getBackend() !== $backend || $object->getUri() !== $calendarURI) { + return $this->move($object, $calendarId, $objectURI, $userId); + } + + $this->checkBackendSupports($backend, \OCA\Calendar\Backend\UPDATE_OBJECT); + + $api = &$this->backends->find($backend)->api; + + if($object->isValid() === false) { + $object->fix(); + } + + $object = $api->updateObject($object, $calendarURI, $objectURI, $userId); + $cacheObjects = $api->cacheObjects($calendarURI, $userId); + if($cacheObjects) { + $this->omp->update($object, $calendarURI, $objectURI, $userId); + } + + $this->calendarBusinessLayer->touch($calendarId, $userId); + + return $object; + } catch(DoesNotImplementException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * delete an object from a calendar + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $objectURI UID of the object + * @param string $userId + * @throws BusinessLayerException + * @return boolean + */ + public function delete(Calendar $calendar, $objectURI, $userId) { + try { + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + + $this->checkBackendEnabled($backend); + + $this->checkBackendSupports($backend, \OCA\Calendar\Backend\DELETE_OBJECT); + + $api = &$this->backends->find($backend)->api; + $api->deleteObject($calendarURI, $objectURI, $userId); + + if($api->cacheObjects($calendarURI, $userId)) { + $this->omp->delete($calendar); + } + + $this->calendarBusinessLayer->touch($calendarId, $userId); + + return true; + } catch(DoesNotImplementException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * moves an object from one to another calendar + * @param Object $object + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $objectURI UID of the object + * @param string $userId + * @throws BusinessLayerException + * @return array containing all items + */ + public function move($object, Calendar $calendar, $objectURI, $userId) { + try { + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + $oldBackend = $backend; + $newBackend = $calendar->getBackend(); + + $oldCalendarURI = $calendarURI; + $newCalendarURI = $object->getCalendarURI(); + + $oldObjectURI = $objectURI; + $newObjectURI = $object->getURI(); + + $this->checkBackendEnabled($oldBackend); + $this->checkBackendEnabled($newBackend); + + $this->allowNoObjectURITwice($newBackend, $newCalendarURI, $newObjectURI, $userId); + + $oldBackendsAPI = &$this->backends->find($oldBackend)->api; + $newBackendsAPI = &$this->backends->find($newBackend)->api; + + $doesBackendSupportMovingEvents = $oldBackendsAPI->implementsActions(\OCA\Calendar\Backend\MOVE_OBJECT); + + if($oldBackend == $newBackend && $doesBackendSupportMovingEvents === true) { + $object = $newBackendsAPI->moveObject($object, $calendarURI, $objectURI, $userId); + } else { + $this->checkBackendSupports($oldBackend, \OCA\Calendar\Backend\DELETE_OBJECT); + $this->checkBackendSupports($newBackend, \OCA\Calendar\Backend\CREATE_OBJECT); + + $status = $newBackendsAPI->createObject($object); + if($status === true) { + $object = $this->backends->find($object->getBackend())->api->createObject(); + } else { + throw new BusinessLayerException('Could not move object to another calendar.'); + } + } + + $cacheObjectsInOldBackend = $oldBackendsAPI->cacheObjects($calendarURI, $userId); + if($cacheObjectsInOldBackend === true) { + //dafuq + $this->omp->delete($object, $calendarURI, $objectURI, $userId); + } + + $cacheObjectsInNewBackend = $newBackendsAPI->cacheObjects($calendarURI, $userId); + if($cacheObjectsInNewBackend === true) { + //dafuq + $this->omp->create($object, $object->getCalendarUri(), $object->getObjectUri(), $userId); + } + + $this->calendarBusinessLayer->touch($calendarId, $userId); + $this->calendarBusinessLayer->touch($object->getCalendarId(), $userId); + + return $object; + } catch(DoesNotImplementException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + public function moveAll() { + /*if($this->doesBackendSupport($oldBackend, \OCA\Calendar\Backend\DELETE_OBJECT) !== true) { + $msg = 'CalendarBusinessLayer::update(): User Error: '; + $msg .= 'Backend does not support deleting objects!'; + throw new BusinessLayerException($msg); + } + if($this->doesBackendSupport($newBackend, \OCA\Calendar\Backend\CREATE_OBJECT) !== true) { + $msg = 'CalendarBusinessLayer::update(): User Error: '; + $msg .= 'Backend does not support creating objects!'; + throw new BusinessLayerException($msg); + }*/ + } + + public function deleteAll() { + + } + + /** + * touch an object + * @param string $calendarId global uri of calendar e.g. local-work + * @param string $objectURI UID of the object + * @param string $userId + * @throws BusinessLayerException + * @return array containing all items + */ + public function touch(Calendar $calendar, $objectURI, $userId) { + try { + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + $this->checkBackendEnabled($backend); + + $object = $this->find($calendarId, $objectURI, $userid); + $object->touch(); + + $this->update($object, $calendarId, $userId); + } catch(DoesNotImplementException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } catch(BackendException $ex) { + throw new BusinessLayerException($ex->getMessage()); + } + } + + /** + * make sure that uri does not already exist when creating a new object + * @param string $backend + * @param string $calendarURI + * @param string $userId + * @return boolean + * @throws BusinessLayerException if uri is already taken + */ + private function allowNoObjectURITwice($backend, $calendarURI, $objectURI, $userId){ + if($this->isObjectURIAvailable($backend, $calendarURI, $objectURI, $userId, true) === false) { + throw new BusinessLayerException('Can not add object: UID already exists'); + } + } + + /** + * checks if a uri is available + * @param string $backend + * @param string $calendarURI + * @param string $objectURI + * @param string $userId + * @return boolean + */ + private function isObjectURIAvailable($backend, $calendarURI, $objectURI, $userId, $checkRemote=false) { + $existingObjects = $this->omp->find($backend, $calendarURI, $objectURI, $userId); + if(count($existingObjects) !== 0) { + return false; + } + + if($checkRemote === true) { + $existingRemoteObjects = $this->backends->find($backend)->api->findObject($calendarURI, $objectURI, $userId); + if(count($existingRemoteObjects) !== 0) { + return false; + } + } + + return true; + } + + /** + * update a specific calendar + * @param string $userId + * @return boolean + */ + public function updateCacheForCalendarFromRemote($calendarId, $userId=null) { + try{ + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + + + return true; + }catch(BackendException $ex) { + + } + } + + /** + * update a specific calendar + * @param string $userId + * @return boolean + */ + public function updateCacheForObjectFromRemote($calendarId, $objectURI, $userId) { + try{ + if(is_array($calendarId)) { + $backend = $calendarId[0]; + $calendarURI = $calendarId[1]; + } else { + list($backend, $calendarURI) = $this->splitCalendarURI($calendarId); + } + + $remoteAPI = &$this->backends->find($backend)->api; + + $doesObjectExistCached = $this->doesExist(array($backend, $calendarURI), $objectURI, $userId, false); + $doesObjectExistRemote = $remoteAPI->doesObjectExist($calendarURI, $objectURI, $userId); + + if($doesObjectExistCached === false && $doesObjectExistRemote === false) { + $msg = 'ObjectBusinessLayer::updateCacheForObjectFromRemote(): '; + $msg .= '"b:' . $backend . ';cu:' . $calendarURI . ';ou:' . $objectURI . '" doesn\'t exist'; + $msg .= 'Neither cached nor remote!'; + throw new DoesNotExistException($msg); + } + + if($doesObjectExistRemote === false) { + $msg = 'ObjectBusinessLayer::updateCacheForObjectFromRemote(): '; + $msg .= 'Object vanished from remote - removing object from cache!'; + //TODO - log debug message + + $cachedObject = $this->find(array($backend, $calendarURI), $objectURI, $userId); + + $this->omp->delete($cachedObject); + return true; + } + + $remoteObject = $remoteAPI->findObject($calendarURI, $objectURI, $userId); + + if($doesCalendarExistCached === false) { + $msg = 'ObjectBusinessLayer::updateCacheForObjectFromRemote(): '; + $msg .= 'Object not cached - creating object from remote!'; + //TODO - log debug message + + $this->omp->insert($remoteCalendar); + return true; + } + + if($cachedObject === null) { + $cachedObject = $this->find(array($backend, $calendarURI), $objectURI, $userId); + } + + if($cachedObject->getEtag() === $remoteObject->getEtag()) { + return true; + } + + $this->omp->update($cachedCalendar); + + return true; + }catch(BackendException $ex) { + + } + } +} \ No newline at end of file diff --git a/controller/calendarcontroller.php b/controller/calendarcontroller.php new file mode 100644 index 000000000..1e108b12d --- /dev/null +++ b/controller/calendarcontroller.php @@ -0,0 +1,206 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Controller; + +use \OCP\AppFramework\Http\Http; + +use \OCA\Calendar\Db\DoesNotExistException; +use \OCA\Calendar\BusinessLayer\BusinessLayerException; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\CalendarCollection; + +use \OCA\Calendar\Http\JSONResponse; + +use \OCA\Calendar\Http\ICS\ICSCalendar; +use \OCA\Calendar\Http\ICS\ICSCalendarCollection; +use \OCA\Calendar\Http\ICS\ICSCalendarReader; + +use \OCA\Calendar\Http\JSON\JSONCalendar; +use \OCA\Calendar\Http\JSON\JSONCalendarCollection; +use \OCA\Calendar\Http\JSON\JSONCalendarReader; + +class CalendarController extends Controller { + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @API + */ + public function index() { + try { + $userId = $this->api->getUserId(); + $limit = $this->header('X-OC-CAL-LIMIT', 'integer'); + $offset = $this->header('X-OC-CAL-OFFSET', 'integer'); + + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + $calendarCollection = $this->calendarBusinessLayer->findAll($userId, $limit, $offset); + if($doesAcceptRawICS === true) { + $serializer = new ICSCalendarCollection($calendarCollection); + } else { + $serializer = new JSONCalendarCollection($calendarCollection); + } + + return new JSONResponse($serializer); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'debug'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @API + */ + public function show() { + try { + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + $calendar = $this->calendarBusinessLayer->find($calendarId, $userId); + if($doesAcceptRawICS === true) { + $serializer = new ICSCalendar($calendar); + } else { + $serializer = new JSONCalendar($calendar); + } + + return new JSONResponse($serializer); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'debug'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @API + */ + public function create() { + try { + $userId = $this->api->getUserId(); + $data = $this->request->params; + + $didSendRawICS = $this->didClientSendRawICS(); + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + if($didSendRawICS === true) { + $reader = new ICSCalendarReader($data); + } else { + $reader = new JSONCalendarReader($data); + } + + var_dump($reader->sanitize()->getObject()); + exit; + + $calendar = $reader->sanitize()->getCalendar(); + $calendar->setUser($userId)->setOwner($userId); + + $calendar = $this->calendarBusinessLayer->create($calendar); + + if($doesAcceptRawICS === true) { + $serializer = new ICSCalendar($calendar); + } else { + $serializer = new JSONCalendar($calendar); + } + + return new JSONResponse($serializer, HTTP::STATUS_CREATED); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'debug'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @API + */ + public function update() { + try { + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + $data = $this->request->params; + + $didSendRawICS = $this->didClientSendRawICS(); + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + //check if calendar exists, if not return 404 + if($this->calendarBusinessLayer->doesExist($calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_NOT_FOUND); + } + + if($didSendRawICS === true) { + $reader = new ICSCalendarReader($data); + } else { + $reader = new JSONCalendarReader($data); + } + + $calendar = $reader->sanitize()->getCalendar(); + $calendar->setUser($userId); + + $calendar = $this->calendarBusinessLayer->update($calendar, $calendarId, $userId); + + if($doesAcceptRawICS === true) { + $serializer = new ICSCalendar($calendar); + } else { + $serializer = new JSONCalendar($calendar); + } + + return new JSONResponse($serializer); + } catch(BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'debug'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } + + /** + /** + * @NoAdminRequired + * @NoCSRFRequired + * @API + */ + public function destroy() { + try { + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + + //check if calendar exists, if not return 404 + if($this->calendarBusinessLayer->doesExist($calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_NOT_FOUND); + } + + $this->calendarBusinessLayer->delete($calendarId, $userId); + + return new JSONResponse(); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'warn'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @API + */ + public function forceUpdate() { + try { + $userId = $this->api->getUserId(); + $this->calendarBusinessLayer->updateCacheForAllFromRemote($userId); + return new JSONResponse(); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'warn'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } +} \ No newline at end of file diff --git a/controller/controller.php b/controller/controller.php new file mode 100644 index 000000000..6329fe48a --- /dev/null +++ b/controller/controller.php @@ -0,0 +1,139 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Controller; + +use \OCP\AppFramework\IAppContainer; +use \OCP\IRequest; + +use \OCA\Calendar\BusinessLayer\BusinessLayer; +use \OCA\Calendar\BusinessLayer\CalendarBusinessLayer; +use \OCA\Calendar\BusinessLayer\ObjectBusinessLayer; + +abstract class Controller extends \OCP\AppFramework\Controller { + + /** + * calendar business layer + * @var \OCA\Calendar\BusinessLayer\CalendarBusinessLayer + */ + protected $calendarBusinessLayer; + + /** + * object business layer + * @var \OCA\Calendar\BusinessLayer\ObjectBusinessLayer + */ + protected $objectBusinessLayer; + + /** + * core api + * @var \OCP\AppFramework\IApi + */ + protected $api; + + /** + * constructor + * @param IAppContainer $app interface to the app + * @param IRequest $request an instance of the request + * @param CalendarBusinessLayer $calendarBusinessLayer + * @param ObjectBusinessLayer $objectBusinessLayer + */ + public function __construct(IAppContainer $app, IRequest $request, + $calendarBusinessLayer=null, $objectBusinessLayer=null){ + + parent::__construct($app, $request); + + $this->api = $app->getCoreApi(); + + if($calendarBusinessLayer instanceof CalendarBusinessLayer) { + $this->calendarBusinessLayer = $calendarBusinessLayer; + } + if($objectBusinessLayer instanceof ObjectBusinessLayer) { + $this->objectBusinessLayer = $objectBusinessLayer; + } + } + + /* + * Lets you access http request header + * @param string $key the key which you want to access in the http request header + * @param mixed $default If the key is not found, this value will be returned + * @return mixed content of header field + */ + protected function header($key, $type='string', $default=null){ + $key = 'HTTP_' . strtoupper($key); + + if(isset($this->request->server[$key]) === false) { + return $default; + } else { + $value = $this->request->server[$key]; + if(strtolower($type) === 'datetime') { + $value = \DateTime::createFromFormat(\DateTime::ISO8601); + } else { + settype($value, $type); + } + return $value; + } + } + + /** + * did user request raw ics instead of json + * @param boolean + */ + protected function doesClientAcceptRawICS() { + $accept = $this->header('accept'); + + //check if text/calendar is in the text + //if not, return false + $textCalendarPosition = stripos($accept, 'text/calendar'); + if($textCalendarPosition === false) { + return false; + } + + //get posistion of application/json and application/calendar+json + $applicationJSONPosition = stripos($accept, 'application/json'); + $applicationCalendarJSONPosition = stripos($accept, 'application/calendar+json'); + + if($applicationJSONPosition === false && $applicationCalendarJSONPosition === false) { + return true; + } + + $firstApplicationPosition = min($applicationJSONPosition, $applicationCalendarJSONPosition); + + return ($firstApplicationPosition < $textCalendarPosition) ? false : true; + } + + /** + * did user request raw ics instead of json + * @param boolean + */ + protected function didClientSendRawICS() { + $contentType = $this->header('content-type'); + + //check if there is some charset info + if(stripos($contentType, ';')) { + $explodeContentType = explode(';', $contentType); + $contentType = $explodeContentType[0]; + } + + $didClientSendRawICS = false; + switch($contentType) { + case 'text/calendar': + $didClientSendRawICS = true; + break; + + case 'application/json': + case 'application/calendar+json': + $didClientSendRawICS = false; + break; + + default: + $didClientSendRawICS = false; + break; + } + + return $didClientSendRawICS; + } +} \ No newline at end of file diff --git a/controller/eventcontroller.php b/controller/eventcontroller.php new file mode 100644 index 000000000..d2d424e55 --- /dev/null +++ b/controller/eventcontroller.php @@ -0,0 +1,31 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Controller; + +use \OCP\AppFramework\IAppContainer; +use \OCP\IRequest; + +use \OCA\Calendar\BusinessLayer\CalendarBusinessLayer; +use \OCA\Calendar\BusinessLayer\ObjectBusinessLayer; +use OCA\Calendar\Db\ObjectType; + +class EventController extends ObjectTypeController { + + /** + * constructor + * @param IAppContainer $app interface to the app + * @param IRequest $request an instance of the request + * @param CalendarBusinessLayer $calendarBusinessLayer + * @param ObjectBusinessLayer $objectBusinessLayer + */ + public function __construct(IAppContainer $app, IRequest $request, + CalendarBusinessLayer $calendarBusinessLayer, + ObjectBusinessLayer $objectBusinessLayer){ + parent::__construct($app, $request, $calendarBusinessLayer, $objectBusinessLayer, ObjectType::EVENT); + } +} \ No newline at end of file diff --git a/controller/journalcontroller.php b/controller/journalcontroller.php new file mode 100644 index 000000000..794c9b0e3 --- /dev/null +++ b/controller/journalcontroller.php @@ -0,0 +1,31 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Controller; + +use \OCP\AppFramework\IAppContainer; +use \OCP\IRequest; + +use \OCA\Calendar\BusinessLayer\CalendarBusinessLayer; +use \OCA\Calendar\BusinessLayer\ObjectBusinessLayer; +use OCA\Calendar\Db\ObjectType; + +class JournalController extends ObjectTypeController { + + /** + * constructor + * @param IAppContainer $app interface to the app + * @param IRequest $request an instance of the request + * @param CalendarBusinessLayer $calendarBusinessLayer + * @param ObjectBusinessLayer $objectBusinessLayer + */ + public function __construct(IAppContainer $app, IRequest $request, + CalendarBusinessLayer $calendarBusinessLayer, + ObjectBusinessLayer $objectBusinessLayer){ + parent::__construct($app, $request, $calendarBusinessLayer, $objectBusinessLayer, ObjectType::JOURNAL); + } +} \ No newline at end of file diff --git a/controller/objectcontroller.php b/controller/objectcontroller.php new file mode 100644 index 000000000..bdcf6217c --- /dev/null +++ b/controller/objectcontroller.php @@ -0,0 +1,266 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Controller; + +use \OCP\AppFramework\Http\Http; + +use \OCA\Calendar\Db\DoesNotExistException; +use \OCA\Calendar\BusinessLayer\BusinessLayerException; + +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectCollection; + +use \OCA\Calendar\Db\Permissions; + +use \OCA\Calendar\Http\JSONResponse; + +use \OCA\Calendar\Http\ICS\ICSObject; +use \OCA\Calendar\Http\ICS\ICSObjectCollection; +use \OCA\Calendar\Http\ICS\ICSObjectReader; + +use \OCA\Calendar\Http\JSON\JSONObject; +use \OCA\Calendar\Http\JSON\JSONObjectCollection; +use \OCA\Calendar\Http\JSON\JSONObjectReader; + +class ObjectController extends Controller { + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @API + */ + public function index() { + try { + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + + $limit = $this->header('X-OC-CAL-LIMIT', 'integer'); + $offset = $this->header('X-OC-CAL-OFFSET', 'integer'); + + $expand = $this->header('X-OC-CAL-EXPAND', 'boolean'); + $start = $this->header('X-OC-CAL-START', 'DateTime'); + $end = $this->header('X-OC-CAL-END', 'DateTime'); + + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + //check if calendar exists, if not return 404 + if($this->calendarBusinessLayer->doesExist($calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_NOT_FOUND); + } + //check if user is allowed to read calendar, if not return 403 + if($this->calendarBusinessLayer->doesAllow(Permissions::READ, $calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_FORBIDDEN); + } + + if($start === null || $end === null) { + $objectCollection = + $this->objectBusinessLayer + ->findAll($calendarId, $userId, + $limit, $offset); + } else { + $objectCollection = + $this->objectBusinessLayer + ->findAllInPeriod($calendarId, $start, + $end, $userId, + $limit, $offset); + } + + if($expand === true) { + $objectCollection->expand($start, $end); + } + + $serializer = ($doesAcceptRawICS === true) ? + new ICSObjectCollection($objectCollection) : + new JSONObjectCollection($objectCollection); + + return new JSONResponse($serializer, Http::STATUS_OK); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'warn'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } + + /** + * @IsAdminExemption + * @IsSubAdminExemption + * @CSRFExemption + * @API + * + * @brief returns $object specified by it's UID + * @return an instance of a Response implementation + */ + public function show() { + try { + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + $objectURI = $this->params('objectId'); + + $expand = $this->header('X-OC-CAL-EXPAND', 'boolean'); + $start = $this->header('X-OC-CAL-START', 'DateTime'); + $end = $this->header('X-OC-CAL-END', 'DateTime'); + + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + //check if calendar and object exists, if not return 404 + if($this->calendarBusinessLayer->doesExist($calendarId, $userId) === false || + $this->objectBusinessLayer->doesExist($calendarId, $objectURI, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_NOT_FOUND); + } + //check if user is allowed to read calendar, if not return 403 + if($this->calendarBusinessLayer->doesAllow(Permissions::READ, $calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_FORBIDDEN); + } + + $object = $this->objectBusinessLayer->find($calendarId, $objectURI, $userId); + + if($expand === true) { + $objectCollection = $object->expand($start, $end); + $serializer = ($doesAcceptRawICS === true) ? + new ICSObjectCollection($objectCollection) : + new JSONObjectCollection($objectCollection); + } else { + $serializer = ($doesAcceptRawICS === true) ? + new ICSObject($object) : + new JSONObject($object); + } + + return new JSONResponse($serializer); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'warn'); + return new JSONResponse(null, Http::STATUS_NOT_FOUND); + } catch (JSONException $ex) { + //do smth + } + } + + /** + * @IsAdminExemption + * @IsSubAdminExemption + * @CSRFExemption + * @API + */ + public function create() { + try { + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + $data = $this->request->params; + + //check if calendar exists, if not return 404 + if($this->calendarBusinessLayer->doesExist($calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_NOT_FOUND); + } + //check if user is allowed to create objects, if not return 403 + if($this->calendarBusinessLayer->doesAllow(PERMISSIONS::CREATE, $calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_FORBIDDEN); + } + + $didSendRawICS = $this->didClientSendRawICS(); + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + if($didSendRawICS === true) { + $reader = new ICSObjectReader($data); + } else { + $reader = new JSONObjectReader($data); + } + + $object = $reader->getObject(); + $type = $object->getType(); + + //check if calendar supports type + + $object = $this->objectBusinessLayer->create($object, $calendarid, $userId); + + //return no content if user is not allowed to read + if($this->calendarBusinessLayer->doesAllow(Permissions::READ, $calendarId, $userId) === false) { + return new JSONResponse(null, Http::STATUS_NO_CONTENT); + } + + $serializer = ($doesAcceptRawICS === true) ? + new ICSObject($object) : + new JSONObject($object); + + return new JSONResponse($serializer, Http::STATUS_CREATED); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'warn'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } + + /** + * @IsAdminExemption + * @IsSubAdminExemption + * @CSRFExemption + * @API + */ + public function update() { + try { + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + $objectURI = $this->params('objectId'); + $data = $this->request->params; + + //check if calendar exists, if not return 404 + if($this->calendarBusinessLayer->doesExist($calendarId, $userId) === false || + $this->objectBusinessLayer->doesExist($calendarId, $objectURI, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_NOT_FOUND); + } + //check if user is allowed to update objects, if not return 403 + if($this->calendarBusinessLayer->doesAllow(PERMISSIONS::UPDATE, $calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_FORBIDDEN); + } + + $didSendRawICS = $this->didClientSendRawICS(); + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + if($didSendRawICS === true) { + $reader = new ICSObjectReader($data); + } else { + $reader = new JSONObjectReader($data); + } + + $object = $reader->getObject(); + + $object = $this->objectBusinessLayer->update($object, $objectURI, $calendarId, $userId); + + //return no content if user is not allowed to read + if($this->calendarBusinessLayer->doesAllow(Permissions::READ, $calendarId, $userId) === false) { + return new JSONResponse(null, Http::STATUS_NO_CONTENT); + } + + $serializer = ($doesAcceptRawICS === true) ? + new ICSObject($object) : + new JSONObject($object); + + return new JSONResponse($serializer); + } catch(BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'warn'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } + + /** + * @IsAdminExemption + * @IsSubAdminExemption + * @CSRFExemption + * @API + */ + public function destroy() { + try { + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + $objectURI = $this->params('objectId'); + + $this->objectBusinessLayer->delete($calendarId, $objectURI, $userId); + + return new JSONResponse(null, HTTP::STATUS_NO_CONTENT); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'warn'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } +} \ No newline at end of file diff --git a/controller/objecttypecontroller.php b/controller/objecttypecontroller.php new file mode 100644 index 000000000..3a752b757 --- /dev/null +++ b/controller/objecttypecontroller.php @@ -0,0 +1,188 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Controller; + +use \OCP\AppFramework\IAppContainer; +use \OCP\IRequest; + +use \OCP\AppFramework\Http\Http; + +use \OCA\Calendar\Db\DoesNotExistException; +use \OCA\Calendar\BusinessLayer\BusinessLayerException; + +use \OCA\Calendar\Db\Object; +use \OCA\Calendar\Db\ObjectCollection; +use \OCA\Calendar\Db\ObjectType; + +use \OCA\Calendar\BusinessLayer\CalendarBusinessLayer; +use \OCA\Calendar\BusinessLayer\ObjectBusinessLayer; + +use \OCA\Calendar\Http\JSONResponse; + +use \OCA\Calendar\Http\ICS\ICSObject; +use \OCA\Calendar\Http\ICS\ICSObjectCollection; +use \OCA\Calendar\Http\ICS\ICSObjectReader; + +use \OCA\Calendar\Http\JSON\JSONObject; +use \OCA\Calendar\Http\JSON\JSONObjectCollection; +use \OCA\Calendar\Http\JSON\JSONObjectReader; + +abstract class ObjectTypeController extends ObjectController { + + /** + * type of object this controller is handling + * @var \OCA\Calendar\Db\ObjectType::... + */ + protected $objectType; + + /** + * constructor + * @param IAppContainer $app interface to the app + * @param IRequest $request an instance of the request + * @param CalendarBusinessLayer $calendarBusinessLayer + * @param ObjectBusinessLayer $objectBusinessLayer + * @param integer $objectType: type of object, use \OCA\Calendar\Db\ObjectType::... + */ + public function __construct(IAppContainer $app, IRequest $request, + CalendarBusinessLayer $calendarBusinessLayer, + ObjectBusinessLayer $objectBusinessLayer, + $type){ + + parent::__construct($app, $request, + $calendarBusinessLayer, + $objectBusinessLayer); + + $this->objectType = $type; + } + + /** + * @IsAdminExemption + * @IsSubAdminExemption + * @CSRFExemption + * @API + */ + public function index() { + try { + var_dump('index called'); + exit; + + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + + $limit = $this->header('X-OC-CAL-LIMIT', 'integer'); + $offset = $this->header('X-OC-CAL-OFFSET', 'integer'); + + $expand = $this->header('X-OC-CAL-EXPAND', 'boolean'); + $start = $this->header('X-OC-CAL-START', 'DateTime'); + $end = $this->header('X-OC-CAL-END', 'DateTime'); + + var_dump($doesAcceptRawICS); + exit; + + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + var_dump($doesAcceptRawICS); + exit; + + //check if calendar exists, if not return 404 + if($this->calendarBusinessLayer->doesExist($calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_NOT_FOUND); + } + //check if user is allowed to read calendar, if not return 403 + if($this->calendarBusinessLayer->doesAllow(Permissions::READ, $calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_FORBIDDEN); + } + + if($start === null || $end === null) { + $objectCollection = + $this->objectBusinessLayer + ->findAllByType($calendarId, $this->objectType, + $userId, $limit, $offset); + } else { + $objectCollection = + $this->objectBusinessLayer + ->findAllByTypeInPeriod($calendarId, $this->objectType, + $start, $end, + $userId, $limit, $offset); + } + + if($expand === true) { + $objectCollection->expand($start, $end); + } + + $serializer = ($doesAcceptRawICS === true) ? + new ICSObjectCollection($objectCollection) : + new JSONObjectCollection($objectCollection); + + return new JSONResponse($serializer, Http::STATUS_OK); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'warn'); + return new JSONResponse(null, Http::STATUS_BAD_REQUEST); + } + } + + /** + * @IsAdminExemption + * @IsSubAdminExemption + * @CSRFExemption + * @API + */ + public function show() { + try { + $userId = $this->api->getUserId(); + $calendarId = $this->params('calendarId'); + $objectId = $this->getObjectId(); + + $expand = $this->header('X-OC-CAL-EXPAND', 'boolean'); + $start = $this->header('X-OC-CAL-START', 'DateTime'); + $end = $this->header('X-OC-CAL-END', 'DateTime'); + + $doesAcceptRawICS = $this->doesClientAcceptRawICS(); + + //check if calendar exists, if not return 404 + if($this->calendarBusinessLayer->doesExist($calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_NOT_FOUND); + } + //check if user is allowed to read calendar, if not return 403 + if($this->calendarBusinessLayer->doesAllow(Permissions::READ, $calendarId, $userId) === false) { + return new JSONResponse(null, HTTP::STATUS_FORBIDDEN); + } + + $object = + $this->objectBusinessLayer + ->findByType($calendarId, $objectId, + $this->objecttype, $userId); + + if($expand === true) { + $objectCollection = $object->expand($start, $end); + $serializer = ($doesAcceptRawICS === true) ? + new ICSObjectCollection($objectCollection) : + new JSONObjectCollection($objectCollection); + } else { + $serializer = ($doesAcceptRawICS === true) ? + new ICSObject($object) : + new JSONObject($object); + } + + return new JSONResponse($serializer); + } catch (BusinessLayerException $ex) { + $this->app->log($ex->getMessage(), 'warn'); + return new JSONResponse(null, HTTP::STATUS_BAD_REQUEST); + } + } + + /** + * @brief get objectId of request + * TODO - find a better solution + * @return $string objectId + */ + private function getObjectId() { + list($routeApp, $routeController, $routeMethod) = explode('.', $this->params('_route')); + return $this->params(substr($routeController, 0, strlen($routeController) - 1) . 'Id'); + } +} \ No newline at end of file diff --git a/controller/settingscontroller.php b/controller/settingscontroller.php new file mode 100644 index 000000000..e69de29bb diff --git a/controller/todocontroller.php b/controller/todocontroller.php new file mode 100644 index 000000000..f04d50a87 --- /dev/null +++ b/controller/todocontroller.php @@ -0,0 +1,31 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Controller; + +use \OCP\AppFramework\IAppContainer; +use \OCP\IRequest; + +use \OCA\Calendar\BusinessLayer\CalendarBusinessLayer; +use \OCA\Calendar\BusinessLayer\ObjectBusinessLayer; +use OCA\Calendar\Db\ObjectType; + +class TodoController extends ObjectTypeController { + + /** + * constructor + * @param IAppContainer $app interface to the app + * @param IRequest $request an instance of the request + * @param CalendarBusinessLayer $calendarBusinessLayer + * @param ObjectBusinessLayer $objectBusinessLayer + */ + public function __construct(IAppContainer $app, IRequest $request, + CalendarBusinessLayer $calendarBusinessLayer, + ObjectBusinessLayer $objectBusinessLayer){ + parent::__construct($app, $request, $calendarBusinessLayer, $objectBusinessLayer, ObjectType::TODO); + } +} \ No newline at end of file diff --git a/controller/viewcontroller.php b/controller/viewcontroller.php new file mode 100644 index 000000000..485270336 --- /dev/null +++ b/controller/viewcontroller.php @@ -0,0 +1,70 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Controller; + +use \OCA\Calendar\BusinessLayer\BackendBusinessLayer; +use \OCA\Calendar\BusinessLayer\CalendarBusinessLayer; +use \OCA\Calendar\BusinessLayer\ObjectBusinessLayer; + +use \OCA\Calendar\BusinessLayer\BusinessLayerException; + +use OCA\Calendar\Db\Calendar; +use OCA\Calendar\Db\JSONCalendar; +use OCA\Calendar\Db\Object; +use OCA\Calendar\Db\JSONObject; + +class ViewController extends Controller { + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @brief renders the index page + * @return an instance of a Response implementation + */ + public function index(){ + var_dump('index_called'); + exit; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @brief renders \DateTimeZone::listAbbreviations(); as JSON + * @return an instance of a JSONResponse implementation + */ + public function timezoneIndex() { + $timezones = \DateTimeZone::listAbbreviations(); + return new JSONResponse($timezones); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @brief saves the new view + * @return an instance of a JSONResponse implementation + */ + public function setView(){ + $newView = $this->param('view'); + switch($newView) { + case 'agendaDay'; + case 'agendaWeek'; + case 'basic2Weeks': + case 'basic4Weeks': + case 'list': + \OCP\Config::setUserValue($this->app->getUserId(), 'calendar', 'currentview', $newView); + return new JSONResponse(array('newView' => $newView)); + break; + default: + return new JSONRespose(array('message'=>'Invalid view'), HTTP::STATUS_BAD_REQUEST); + break; + } + } +} \ No newline at end of file diff --git a/db/backend.php b/db/backend.php new file mode 100644 index 000000000..0aeab91f5 --- /dev/null +++ b/db/backend.php @@ -0,0 +1,118 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \OCA\Calendar\Backend\IBackend; +use \OCA\Calendar\Sabre\VObject\Component\VCalendar; + +class Backend extends Entity { + + public $id; + public $backend; + public $classname; + public $arguments; + public $enabled; + + public $api; + + /** + * @brief init Backend object with data from db row + * @param array $fromRow + */ + public function __construct($fromRow=null){ + if(is_array($fromRow)){ + $this->fromRow($fromRow); + } + + $this->api = null; + } + + /** + * registers an API for a backend + * @param CalendarInterface $api + * @return Backend + */ + public function registerAPI(IBackend $api){ + $this->api = $api; + return $this; + } + + /** + * @brief take data from VObject and put into this Calendar object + * @return VCalendar Object + */ + public function fromVObject(VCalendar $vcalendar) { + $msg = 'Can\'t create backend from vobject!'; + throw new \BadFunctionCallException($msg); + } + + /** + * @brief get VObject from Calendar Object + * @return VCalendar Object + */ + public function getVObject() { + $msg = 'Can\'t create vobject from backend!'; + throw new \BadFunctionCallException($msg); + } + + /** + * disables a backend + * @return Backend + */ + public function disable() { + $this->setEnabled(false); + return $this; + } + + /** + * enables a backend + * @return Backend + */ + public function enable() { + $this->setEnabled(true); + return $this; + } + + /** + * @brief check if object is valid + * @return boolean + */ + public function isValid() { + if(is_string($this->backend) === false) { + return false; + } + if(trim($this->backend) === '') { + return false; + } + + if(is_string($this->classname) === false) { + return false; + } + if(class_exists($this->classname) === false) { + return false; + } + + if(is_array($this->arguments) === false && $this->arguments !== null) { + return false; + } + + if(is_bool($this->enabled) === false) { + return false; + } + + if(($this->api instanceof IBackend) === false && $this->api !== null) { + return false; + } + + return true; + } + + public function __toString() { + return $this->backend . '::' . $this->classname; + } +} \ No newline at end of file diff --git a/db/backendcollection.php b/db/backendcollection.php new file mode 100644 index 000000000..13332cb60 --- /dev/null +++ b/db/backendcollection.php @@ -0,0 +1,35 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +class BackendCollection extends Collection { + + /** + * @brief get a collection of all enabled backends within collection + * @return BackendCollection of all enabled backends + */ + public function enabled() { + return $this->search('enabled', true); + } + + /** + * @brief get a collection of all disabled backends within collection + * @return BackendCollection of all disabled backends + */ + public function disabled() { + return $this->search('enabled', false); + } + + /** + * @brief find backend by name + * @return Backend object + */ + public function find($backendName) { + return $this->search('backend', $backendName)->current(); + } +} \ No newline at end of file diff --git a/db/backendmapper.php b/db/backendmapper.php new file mode 100644 index 000000000..fd186e6c1 --- /dev/null +++ b/db/backendmapper.php @@ -0,0 +1,121 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \OCP\AppFramework\IAppContainer; + +use \OCA\Calendar\Db\Backend; +use \OCA\Calendar\Db\BackendCollection; + +class BackendMapper extends Mapper { + + protected $api; + protected $configName; + + private $backendCollection; + private $didChange; + + /** + * @brief Constructor + * @param API $api: Instance of the API abstraction layer + */ + public function __construct(IAppContainer $api, $configName='calendar_backends'){ + $this->api = $api; + $this->configName = $configName; + + $backends = \OCP\Config::getSystemValue($configName); + if($backends === null) { + $backends = $api->query('fallbackBackendConfig'); + } + + $backendCollection = new BackendCollection(); + foreach($backends as $id => $backend) { + $backend = new Backend($backend); + $backend->setId($id); + + $backendCollection->add($backend); + } + $this->backendCollection = $backendCollection; + + $this->didChange = false; + } + + /** + * @brief Destructor - write changes + * @param API $api: Instance of the API abstraction layer + */ + public function __destruct() { + if($this->didChange === true) { + $newConfig = $this->backendCollection->getObjects(); + //\OCP\Config::setSystemValue($this->configName, $newConfig); + } + } + + /** + * @brief Finds an item by it's name + * @param string $backend name of backend + * @throws DoesNotExistException: if the item does not exist + * @return the backend item + */ + public function find($backend){ + return $this->backendCollection->find($backend); + } + + /** + * Finds all Items + * @return array containing all items + */ + public function findAll(){ + return $this->backendCollection; + } + + /** + * Finds all Items where enabled is ? + * @return array containing all items where enabled is ? + */ + public function findWhereEnabledIs($isEnabled){ + return $this->backendCollection->search('enabled', $isEnabled); + } + + /** + * Saves an item into the database + * @param Item $item: the item to be saved + * @return $this + */ + public function insert(Entity $item){ + $this->backendCollection->add($item); + + $this->didChange = true; + return $this; + } + + /** + * Updates an item + * @param Item $item: the item to be updated + * @return $this + */ + public function update(Entity $item){ + $this->backendCollection->removeByProperty('id', $item->getId()); + $this->backendCollection->add($item); + + $this->didChange = true; + return $this; + } + + /** + * Deletes an item + * @param Entity $item: the item to be deleted + * @return $this + */ + public function delete(Entity $item){ + $this->backendCollection->removeByEntity($item); + + $this->didChange = true; + return $this; + } +} \ No newline at end of file diff --git a/db/calendar.php b/db/calendar.php new file mode 100644 index 000000000..62c038a3b --- /dev/null +++ b/db/calendar.php @@ -0,0 +1,261 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \OCA\Calendar\Sabre\VObject\Component\VCalendar; + +use \OCA\Calendar\Utility\CalendarUtility; +use \OCA\Calendar\Utility\Utility; + +class Calendar extends Entity { + + public $id; + public $userId; + public $ownerId; + public $backend; + public $uri; + public $displayname; + public $components; + public $ctag; + public $timezone; + public $color; + public $order; + public $enabled; + public $cruds; + + /** + * @brief init Calendar object with data from db row + * @param mixed (array / VCalendar) $createFrom + */ + public function __construct($createFrom=null){ + $this->addType('userId', 'string'); + $this->addType('ownerId', 'string'); + $this->addType('backend', 'string'); + $this->addType('uri', 'string'); + $this->addType('displayname', 'string'); + $this->addType('components', 'integer'); + $this->addType('ctag', 'integer'); + $this->addType('color', 'string'); + $this->addType('order', 'integer'); + $this->addType('enabled', 'boolean'); + $this->addType('cruds', 'integer'); + + //create from array + if(is_array($createFrom)){ + $this->fromRow($createFrom); + } + + //create from vcalendar + if($createFrom instanceof VCalendar) { + $this->fromVObject($createFrom); + } + } + + + /** + * @brief take data from VObject and put into this Calendar object + * @param \Sabre\VObject\Component\VCalendar $vcalendar + * @return $this + */ + public function fromVObject(VCalendar $vcalendar) { + if(isset($vcalendar->{'X-WR-CALNAME'})) { + $this->setDisplayname($vcalendar->{'X-WR-CALNAME'}); + } + + if(isset($vcalendar->{'X-WR-TIMEZONE'})) { + try { + $this->setTimezone(new Timezone($vcalendar->{'X-WR-TIMEZONE'})); + } catch(\Exception $ex) {} + } + + if(isset($calendar->{'X-APPLE-CALENDAR-COLOR'})) { + $this->setColor($vcalendar->{'X-APPLE-CALENDAR-COLOR'}); + } + + return $this; + } + + + /** + * @brief get VObject from Calendar Object + * @return \Sabre\VObject\Component\VCalendar object + */ + public function getVObject() { + $properties = array( + 'X-OC-ID' => $this->getId(), + 'X-OC-USERID' => $this->getUserId(), + 'X-OC-OWNERID' => $this->getOwnerId(), + 'X-OC-BACKEND' => $this->getBackend(), + 'X-OC-URI' => $this->getUri(), + 'X-WR-CALNAME' => $this->getDisplayname(), + 'X-OC-COMPS' => ObjectType::getAsString($this->getComponents()), + 'X-OC-CTAG' => $this->getCtag(), + //'X-WR-TIMEZONE' => $this->getTimezone(), + 'X-APPLE-CALENDAR-COLOR' => $this->getColor(), + 'X-OC-ORDER' => $this->getOrder(), + 'X-OC-ENABLED' => $this->getEnabled(), + 'X-OC-CRUDS' => $this->getCruds() + ); + $vcalendar = new VCalendar($properties); + //$vcalendar->addComponent($this->timezone->getVObject()); + + return $vcalendar; + } + + + /** + * @brief increment ctag + * @return $this + */ + public function touch() { + $this->ctag++; + return $this; + } + + + /** + * @brief set uri property + */ + public function setURI($uri) { + if(is_string($uri) === false) { + return null; + } + $this->uri = $uri; + parent::setUri($this->slugify('uri')); + } + + /** + * @brief set timezone + * @param \OCA\Calendar\Db\Timezone $timezone + */ + public function setTimezone(Timezone $timezone) { + $this->timezone = $timezone; + return $this; + } + + /** + * @brief get calendarId of object + * @return string + */ + public function getCalendarId(){ + if(!property_exists($this, 'backend') || !property_exists($this, 'uri')) { + $msg = 'getCalendarId is not applicable to this kind of object'; + throw new \BadFunctionCallException($msg); + } + + $backend = $this->backend; + $calendarURI = $this->uri; + + $calendarId = CalendarUtility::getURI($backend, $calendarURI); + + return $calendarId; + } + + + /** + * @brief update backend and uri by calendarId + * @param string $calendarId + * @return mixed + * $this if calendarId was set + * false if calendarId could not be set + */ + public function setCalendarId($calendarId) { + if(!property_exists($this, 'backend') || !property_exists($this, 'uri')) { + $msg = 'setCalendarId is not applicable to this kind of object'; + throw new \BadFunctionCallException($msg); + } + + list($backend, $calendarURI) = CalendarUtility::splitURI($calendarId); + + if($backend !== false && $calendarURI !== false) { + $this->backend = $backend; + $this->uri = $calendarURI; + + return $this; + } + + return false; + } + + /** + * @brief check if object is valid + * @return boolean + */ + public function isValid() { + $strings = array( + $this->userId, + $this->ownerId, + $this->backend, + $this->uri, + $this->displayname, + $this->color, + ); + + foreach($strings as $string) { + if(is_string($string) === false) { + return false; + } + if(trim($string) === '') { + return false; + } + } + + $uInts = array( + $this->components, + $this->ctag, + $this->order, + $this->cruds, + ); + + foreach($uInts as $integer) { + if(is_int($integer) === false) { + return false; + } + if($integer < 0) { + return false; + } + } + + $booleans = array( + $this->enabled, + ); + + foreach($booleans as $boolean) { + if(is_bool($boolean) === false) { + return false; + } + } + + if(preg_match('/[A-Za-z0-9]+/', $this->uri) !== 1) { + return false; + } + + if(preg_match('/#((?:[0-9a-fA-F]{2}){3}|(?:[0-9a-fA-F]{1}){3}|(?:[0-9a-fA-F]{1}){4}|(?:[0-9a-fA-F]{2}){4})$/', $this->color) !== 1) { + return false; + } + + if($this->components > ObjectType::ALL) { + return false; + } + + if($this->cruds > Permissions::ALL) { + return false; + } + + if($this->timezone instanceof Timezone && $this->timezone->isValid() === false) { + return false; + } + + return true; + } + + + public function __toString() { + return $this->userId . '::' . $this->getCalendarId(); + } +} \ No newline at end of file diff --git a/db/calendarcollection.php b/db/calendarcollection.php new file mode 100644 index 000000000..0ecd5a192 --- /dev/null +++ b/db/calendarcollection.php @@ -0,0 +1,87 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \OCA\Calendar\Sabre\VObject\Component\VCalendar; + +class CalendarCollection extends Collection { + + /** + * @brief get a collection of all enabled calendars within collection + * @return CalendarCollection of all enabled calendars + */ + public function enabled() { + return $this->search('enabled', true); + } + + /** + * @brief get a collection of all disabled calendars within collection + * @return CalendarCollection of all disabled calendars + */ + public function disabled() { + return $this->search('enabled', false); + } + + /** + * @brief get a collection of all calendars owned by a certian user + * @param string userId of owner + * @return CalendarCollection of all calendars owned by user + */ + public function ownedBy($userId) { + return $this->search('ownerId', $userId); + } + + /** + * @brief get a collection of calendars that supports certian components + * @param int $component use \OCA\Calendar\Db\ObjectType to get wanted component code + * @return CalendarCollection of calendars that supports certian components + */ + public function components($component) { + $newCollection = new CalendarCollection(); + + $this->iterate(function($object) use (&$newCollection, $component) { + if($object->getComponents() & $component) { + $newCollection->add(clone $object); + } + }); + + return $newCollection; + } + + /** + * @brief get a collection of calendars with a certain permission + * @param int $cruds use \OCA\Calendar\Db\Permissions to get wanted permission code + * @return CalendarCollection of calendars with a certian permission + */ + public function permissions($cruds) { + $newCollection = new CalendarCollection(); + + $this->iterate(function($object) use (&$newCollection, $cruds) { + if($object->getCruds() & $cruds) { + $newCollection->add(clone $object); + } + }); + + return $newCollection; + } + + public function filterByBackends(BackendCollection $backends) { + $filteredCalendars = new CalendarCollection(); + $objects = $backends->getObjects(); + + $this->iterate(function($object) use (&$filteredCalendars, $objects) { + foreach($objects as $backend) { + if($object->getBackend() === $backend->getBackend()) { + $filteredCalendars->add(clone $object); + } + } + }); + + return $filteredCalendars; + } +} \ No newline at end of file diff --git a/db/calendarmapper.php b/db/calendarmapper.php new file mode 100644 index 000000000..b0096ab40 --- /dev/null +++ b/db/calendarmapper.php @@ -0,0 +1,168 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \OCP\AppFramework\IAppContainer; + +use \OCA\Calendar\Db\Calendar; + +class CalendarMapper extends Mapper { + + /** + * @param API $api: Instance of the API abstraction layer + */ + public function __construct($api, $tablename='clndr_calcache'){ + parent::__construct($api, $tablename); + } + + /** + * find calendar by backend, uri and userId + * @param string $backend + * @param string $uri + * @param string $userId + * @throws DoesNotExistException: if the item does not exist + * @throws MultipleObjectsReturnedException: if more than one item found + * @return calendar object + */ + public function find($backend, $uri, $userId){ + $sql = 'SELECT * FROM `' . $this->getTableName() . '` '; + $sql .= 'WHERE `backend` = ? AND `uri` = ? AND `user_id` = ?'; + + $row = $this->findOneQuery($sql, array($backend, $uri, $userId)); + + return new Calendar($row); + } + + /** + * find all calendars of a user + * @param string $userId + * @param integer $limit + * @param integer $offset + * @return CalendarCollection + */ + public function findAll($userId, $limit, $offset){ + $sql = 'SELECT * FROM `'. $this->getTableName() . '` '; + $sql .= 'WHERE `user_id` = ? ORDER BY `order`'; + + return $this->findEntities($sql, array($userId), $limit, $offset); + } + + /** + * find all calendars of a user on a backend + * @param string $backend + * @param string $userId + * @param integer $limit + * @param integer $offset + * @return CalendarCollection + */ + public function findAllOnBackend($backend, $userId, $limit, $offset) { + $sql = 'SELECT * FROM `'. $this->getTableName() . '` '; + $sql .= 'WHERE `backend` = ? AND `user_id` = ? ORDER BY `order`'; + + return $this->findEntities($sql, array($backend, $userId), $limit, $offset); + } + + /** + * number of calendars by user + * @param string $userId + * @throws DoesNotExistException: if the item does not exist + * @return integer + */ + public function count($userId){ + $sql = 'SELECT COUNT(*) AS `count` FROM '; + $sql .= '`' . $this->getTableName() . '` WHERE `user_id` = ?'; + + $row = $this->findOneQuery($sql, array($userId)); + + return intval($row['count']); + } + + /** + * number of calendars by user on a backend + * @param string $backend + * @param string $userId + * @throws DoesNotExistException: if the item does not exist + * @return integer + */ + public function countOnBackend($backend, $userId) { + $sql = 'SELECT COUNT(*) AS `count` FROM '; + $sql .= '`' . $this->getTableName() . '` WHERE `backend` = ? AND `user_id` = ?'; + + $row = $this->findOneQuery($sql, array($backend, $userId)); + + return intval($row['count']); + } + + /** + * does a calendar exist + * @param string $backend + * @param string $calendarURI + * @param string $userId + * @throws DoesNotExistException: if the item does not exist + * @return boolean + */ + public function doesExist($backend, $calendarURI, $userId) { + $sql = 'SELECT COUNT(*) AS `count` FROM `' . $this->tableName . '`'; + $sql .= ' WHERE `backend` = ? AND `uri` = ? AND `user_id` = ?'; + + $row = $this->findOneQuery($sql, array($backend, $calendarURI, $userId)); + + $count = intval($row['count']); + if($count === 0) { + return false; + } else { + return true; + } + } + + /** + * checks if a calendar allows a certain action + * @param integer $cruds + * @param string $backend + * @param string $calendarURI + * @param string $userId + * @throws DoesNotExistException: if the item does not exist + * @return boolean + */ + public function doesAllow($cruds, $backend, $calendarURI, $userId) { + $sql = 'SELECT COUNT(*) AS `count` FROM `' . $this->tableName . '`'; + $sql .= ' WHERE `cruds` & ? AND `backend` = ? AND `uri` = ? AND `user_id` = ?'; + + $row = $this->findOneQuery($sql, array($cruds, $backend, $calendarURI, $userId)); + + $count = intval($row['count']); + if($count === 0) { + return false; + } else { + return true; + } + } + + /** + * checks if a calendar supports a certian component + * @param integer $component + * @param string $backend + * @param string $calendarURI + * @param string $userId + * @throws DoesNotExistException: if the item does not exist + * @return boolean + */ + public function doesSupport($component, $backend, $calendarURI, $userId) { + $sql = 'SELECT COUNT(*) AS `count` FROM `' . $this->tableName . '`'; + $sql .= ' WHERE `components` & ? AND `backend` = ? AND `uri` = ? AND `user_id` = ?'; + + $row = $this->findOneQuery($sql, array($component, $backend, $calendarURI, $userId)); + + $count = intval($row['count']); + if($count === 0) { + return false; + } else { + return true; + } + } +} \ No newline at end of file diff --git a/db/collection.php b/db/collection.php new file mode 100644 index 000000000..aa062c8c1 --- /dev/null +++ b/db/collection.php @@ -0,0 +1,357 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +abstract class Collection { + + protected $objects; + + public function __construct($objects=null) { + $this->objects = array(); + + if($objects !== null) { + + if(is_array($objects) === true) { + $this->objects = $objects; + } else if($objects instanceof Entity) { + $this->add($objects); + } else if($objects instanceof Collection) { + $this->addCollection($objects); + } + + } + } + + /** + * @brief add entity to collection + * @param Entity $object entity to be added + * @param integer $nth insert at index, if not set, entity will be appended + * @return integer + */ + public function add(Entity $object, $nth=null) { + if($nth === null) { + $nth = $this->count(); + } + for($i = $this->count(); $i > $nth; $i--) { + $this->objects[$i] = $this->objects[($i - 1)]; + } + $this->objects[$nth] = $object; + return $this; + } + + /** + * @brief add entities to collection + * @param Collection $objects collection of entities to be added + * @param integer $nth insert at index, if not set, entity will be appended + * @return integer + */ + public function addCollection(Collection $objects, $nth=null) { + if($nth === null) { + $nth = $this->count(); + } + $numberOfNewEntities = $objects->count(); + for($i = ($this->count() + $numberOfNewEntities); $i > ($nth + $numberOfNewEntities); $i--) { + $this->objects[$i] = $this->objects[($i - $numberOfNewEntities)]; + } + for($i = $nth; $i < ($nth + $numberOfNewEntities); $i++) { + $this->objects[$i] = $objects->current(); + $objects->next(); + } + return $this; + } + + /** + * @brief remove entity from collection + * @param integer $nth remove nth element, if not set, current element will be removed + * @return + */ + public function remove($nth=null) { + if($nth === null){ + $nth = $this->key(); + } + unset($this->objects[$nth]); + return $this; + } + + /** + * @brief remove entity by it's information + * @param integer $nth remove nth element, if not set, current element will be removed + * @return this + */ + public function removeByEntity(Entity $entity) { + for($i = 0; $i < $this->count(); $i++) { + //use of (==) instead of (===) is intended! + //see http://php.net/manual/en/language.oop5.object-comparison.php + if($this->objects[$i] == $entity) { + unset($this->objects[($i--)]); + } + } + + return $this; + } + + /** + * @brief remove entities by a single property + * @param string $key key for property + * @param mixed $value value to be set + * @return this; + */ + public function removeByProperty($key, $value) { + $propertyGetter = 'get' . ucfirst($key); + + for($i = 0; $i < $this->count(); $i++) { + if(is_callable(array($this->objects[$i], $propertyGetter)) && $object->{$propertyGetter}() === $value) { + unset($this->objects[($i--)]); + } + } + + return $this; + } + + /** + * @brief get number of elements within collection + * @return integer + */ + public function count() { + return count($this->objects); + } + + /** + * @brief get current entity + * @return single Entity + */ + public function current() { + return current($this->objects); + } + + /** + * @brief get num index of current entity + * @return integer + */ + public function key() { + return key($this->objects); + } + + /** + * @brief goto next entity and get it + * @return single Entity + */ + public function next() { + return next($this->objects); + } + + /** + * @brief goto previous entity and get it + * @return single Entity + */ + public function prev() { + return prev($this->objects); + } + + /** + * @brief goto first entity and get it + * @return single Entity + */ + public function reset() { + return reset($this->objects); + } + + /** + * @brief goto last entity and get it + * @return single Entity + */ + public function end() { + return end($this->objects); + } + + /** + * @brief get nth entity of collection + * @param integer $nth + * @return mixed (single Entity) or null + */ + public function get($nth) { + if(array_key_exists($nth, $this->objects)) { + return $this->objects[$nth]; + } else { + return null; + } + } + + /** + * @brief get array of all entities + * @return array of Entities + */ + public function getObjects() { + return $this->objects; + } + + /** + * @brief get a subset of current collection + * @param int $limit + * @param int @offset + * @return array of Entities + */ + public function subset($limit=null, $offset=null) { + $class = get_class($this); + + if($offset === null) { + $offset = 0; + } + + if($limit === null) { + return $this; + } else { + $subset = new $class(); + + for($i = $offset; $i < ($offset + $limit); $i++) { + if(array_key_exists($i, $this->objects)) { + $subset->add($this->objects[$i]); + } + } + + return $subset; + } + + } + + /** + * @brief check if entity is in collection + * @return boolean + */ + public function inCollection(Entity $object) { + var_dump($object); + var_dump($this->objects); + exit; + return in_array($object, $this->objects); + } + + /** + * @brief get one VCalendar object containing all information + * @return VCalendar object + */ + public function getVObject() { + $vObject = new VCalendar(); + + foreach($this->objects as &$object) { + $vElement = $object->getVObject(); + $children = $vElement->getChildren(); + foreach($children as $child) { + $vObject->add($child); + } + } + + return $vobject; + } + + /** + * @brief get an array of VCalendar objects + * @return array of VCalendar object + */ + public function getVObjects() { + $vobjects = array(); + + foreach($this->objects as &$object) { + $vobjects[] = $object->getVObject(); + } + + return $vobjects; + } + + /** + * @brief get a collection of entities that meet criteria + * @param string $key property that's supposed to be searched + * @param mixed $value expected value, can be a regular expression when 3rd param is set to true + * @param boolean $regex disable / enable search based on regular expression + * @return collection + */ + public function search($key, $value, $regex=false) { + $class = get_class($this); + $matchingObjects = new $class(); + + $propertyGetter = 'get' . ucfirst($key); + + foreach($this->objects as &$object) { + if(is_callable(array($object, $propertyGetter)) && $object->{$propertyGetter}() === $value) { + $matchingObjects->add($object); + } + } + + return $matchingObjects; + } + + /** + * @brief get a collection of entities that meet criteria; search calendar data + * @param string $class class of new collection + * @param string $dataProperty name of property that stores data + * @param string $regex regular expression + * @return ObjectCollection + */ + public function searchData($dataProperty, $regex) { + $class = get_class($this); + $matchingObjects = new $class(); + + $dataGetter = 'get' . ucfirst($dataProperty); + + foreach($this->objects as &$object) { + if(is_callable(array($object, $propertyGetter)) && preg_match($regex, $object->{$dataGetter}()) === 1) { + $matchingObjects->add($object); + } + } + + return $matchingObjects; + } + + /** + * @brief set a property for all calendars + * @param string $key key for property + * @param mixed $value value to be set + * @return this + */ + public function setProperty($key, $value) { + $propertySetter = 'set' . ucfirst($key); + + foreach($this->objects as &$object) { + if(is_callable(array($object, $propertySetter))) { + $object->{$propertySetter}($value); + } + } + + return $this; + } + + /** + * @brief checks if all entities are valid + * Stops when it finds the first invalid one + * @param boolean + */ + public function isValid() { + foreach($this->objects as &$object) { + if($object->isValid() === false) { + return false; + } + } + + return true; + } + + /** + * @brief iterate over each entity of collection + * @param anonymous function + * iterate gives you a pointer to the current object. be careful! + * @param array of parameters + */ + public function iterate($function) { + foreach($this->objects as &$object) { + $function($object); + } + } + + public function noDuplicates() { + $this->objects = array_unique($this->objects); + } +} \ No newline at end of file diff --git a/db/doesnotexistexception.php b/db/doesnotexistexception.php new file mode 100644 index 000000000..4392121bc --- /dev/null +++ b/db/doesnotexistexception.php @@ -0,0 +1,10 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +class DoesNotExistException extends \Exception {} \ No newline at end of file diff --git a/db/entity.php b/db/entity.php new file mode 100644 index 000000000..97711dd0c --- /dev/null +++ b/db/entity.php @@ -0,0 +1,250 @@ + + * Copyright (c) 2014 Georg Ehrke + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \OCA\Calendar\Sabre\VObject\Component\VCalendar; + +abstract class Entity { + + public $id; + + protected $updatedFields = array(); + protected $fieldTypes = array('id' => 'int'); + + + /** + * Simple alternative constructor for building entities from a request + * @param array $params the array which was obtained via $this->params('key') + * in the controller + * @return Entity + */ + public static function fromParams(array $params) { + $instance = new static(); + + foreach($params as $key => $value) { + $method = 'set' . ucfirst($key); + $instance->$method($value); + } + + return $instance; + } + + + /** + * Maps the keys of the row array to the attributes + * @param array $row the row to map onto the entity + */ + public function fromRow(array $row){ + foreach($row as $key => $value){ + $prop = $this->columnToProperty($key); + if($value !== null && array_key_exists($prop, $this->fieldTypes)){ + settype($value, $this->fieldTypes[$prop]); + } + $this->$prop = $value; + } + return $this; + } + + abstract public function fromVObject(VCalendar $vcalendar); + + abstract public function getVObject(); + + /** + * @brief overwrite current objects with properties + * from $object that are not null + * @param \OCA\Calendar\Db\Entity $object + * @return $this + */ + public function overwriteWith(Entity $object) { + $properties = get_object_vars($this); + + unset($properties['id']); + + foreach($properties as $key => $value) { + $getter = 'get' . ucfirst($key); + $setter = 'set' . ucfirst($key); + + $newValue = $object->$getter(); + if($newValue !== null && $newValue !== $value) { + $this->$setter($newValue); + } + } + + return $this; + } + + + /** + * @brief checks if current object contains null values + * @return boolean + */ + public function doesContainNullValues() { + $properties = get_object_vars($this); + + foreach($properties as $property) { + $method = 'get' . ucfirst($property); + + if($this->$method() === null) { + return true; + } + } + + return false; + } + + /** + * Marks the entity as clean needed for setting the id after the insertion + */ + public function resetUpdatedFields(){ + $this->updatedFields = array(); + } + + protected function setter($name, $args) { + // setters should only work for existing attributes + if(property_exists($this, $name)){ + $this->markFieldUpdated($name); + + // if type definition exists, cast to correct type + if($args[0] !== null && array_key_exists($name, $this->fieldTypes)) { + settype($args[0], $this->fieldTypes[$name]); + } + $this->$name = $args[0]; + + } else { + throw new \BadFunctionCallException($name . + ' is not a valid attribute'); + } + } + + + protected function getter($name) { + // getters should only work for existing attributes + if(property_exists($this, $name)){ + return $this->$name; + } else { + throw new \BadFunctionCallException($name . + ' is not a valid attribute'); + } + } + + + /** + * Each time a setter is called, push the part after set + * into an array: for instance setId will save Id in the + * updated fields array so it can be easily used to create the + * getter method + */ + public function __call($methodName, $args){ + $attr = lcfirst( substr($methodName, 3) ); + + if(strpos($methodName, 'set') === 0){ + $this->setter($attr, $args); + } elseif(strpos($methodName, 'get') === 0) { + return $this->getter($attr); + } else { + throw new \BadFunctionCallException($methodName . + ' does not exist'); + } + + } + + + /** + * Mark am attribute as updated + * @param string $attribute the name of the attribute + */ + protected function markFieldUpdated($attribute){ + $this->updatedFields[$attribute] = true; + } + + + /** + * Transform a database columnname to a property + * @param string $columnName the name of the column + * @return string the property name + */ + public function columnToProperty($columnName){ + $parts = explode('_', $columnName); + $property = null; + + foreach($parts as $part){ + if($property === null){ + $property = $part; + } else { + $property .= ucfirst($part); + } + } + + return $property; + } + + + /** + * Transform a property to a database column name + * @param string $property the name of the property + * @return string the column name + */ + public function propertyToColumn($property){ + $parts = preg_split('/(?=[A-Z])/', $property); + $column = null; + + foreach($parts as $part){ + if($column === null){ + $column = $part; + } else { + $column .= '_' . lcfirst($part); + } + } + + return $column; + } + + + /** + * @return array array of updated fields for update query + */ + public function getUpdatedFields(){ + return $this->updatedFields; + } + + + /** + * Adds type information for a field so that its automatically casted to + * that value once its being returned from the database + * @param string $fieldName the name of the attribute + * @param string $type the type which will be used to call settype() + */ + protected function addType($fieldName, $type){ + $this->fieldTypes[$fieldName] = $type; + } + + + /** + * Slugify the value of a given attribute + * Warning: This doesn't result in a unique value + * @param string $attributeName the name of the attribute, which value should be slugified + * @return string slugified value + */ + public function slugify($attributeName){ + // toSlug should only work for existing attributes + if(property_exists($this, $attributeName)){ + $value = $this->$attributeName; + // replace everything except alphanumeric with a single '-' + $value = preg_replace('/[^A-Za-z0-9]+/', '-', $value); + $value = strtolower($value); + // trim '-' + return trim($value, '-'); + } else { + throw new \BadFunctionCallException($attributeName . + ' is not a valid attribute'); + } + } + + abstract public function isValid(); +} \ No newline at end of file diff --git a/db/mapper.php b/db/mapper.php new file mode 100644 index 000000000..4cd547dd1 --- /dev/null +++ b/db/mapper.php @@ -0,0 +1,247 @@ + + * Copyright (c) 2014 Morris Jobke + * Copyright (c) 2014 Georg Ehrke + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \OCP\AppFramework\IAppContainer; + +/** + * Simple parent class for inheriting your data access layer from. This class + * may be subject to change in the future + */ +abstract class Mapper { + + protected $tableName; + + /** + * @param API $api Instance of the API abstraction layer + * @param string $tableName the name of the table. set this to allow entity + * queries without using sql + */ + public function __construct(IAppContainer $api, $tableName){ + $this->api = $api; + $this->tableName = '*PREFIX*' . $tableName; + } + + + /** + * @return string the table name + */ + public function getTableName(){ + return $this->tableName; + } + + + /** + * Deletes an entity from the table + * @param Entity $entity the entity that should be deleted + */ + public function delete(Entity $entity){ + $sql = 'DELETE FROM `' . $this->tableName . '` WHERE `id` = ?'; + $this->execute($sql, array($entity->getId())); + } + + + /** + * Creates a new entry in the db from an entity + * @param Entity $enttiy the entity that should be created + * @return the saved entity with the set id + */ + public function insert(Entity $entity){ + // get updated fields to save, fields have to be set using a setter to + // be saved + $properties = $entity->getUpdatedFields(); + $values = ''; + $columns = ''; + $params = array(); + + // build the fields + $i = 0; + foreach($properties as $property => $updated) { + $column = $entity->propertyToColumn($property); + $getter = 'get' . ucfirst($property); + + $columns .= '`' . $column . '`'; + $values .= '?'; + + // only append colon if there are more entries + if($i < count($properties)-1){ + $columns .= ','; + $values .= ','; + } + + array_push($params, $entity->$getter()); + $i++; + + } + + $sql = 'INSERT INTO `' . $this->tableName . '`(' . + $columns . ') VALUES(' . $values . ')'; + + $this->execute($sql, $params); + + $entity->setId((int) \OCP\DB::insertid($this->tableName)); + return $entity; + } + + + + /** + * Updates an entry in the db from an entity + * @throws \InvalidArgumentException if entity has no id + * @param Entity $enttiy the entity that should be created + */ + public function update(Entity $entity){ + // entity needs an id + $id = $entity->getId(); + if($id === null){ + throw new \InvalidArgumentException( + 'Entity which should be updated has no id'); + } + + // get updated fields to save, fields have to be set using a setter to + // be saved + $properties = $entity->getUpdatedFields(); + + // dont update the id field + unset($properties['id']); + + if(count($properties) === 0) { + return; + } + + $columns = ''; + $params = array(); + + // build the fields + $i = 0; + foreach($properties as $property => $updated) { + + $column = $entity->propertyToColumn($property); + $getter = 'get' . ucfirst($property); + + $columns .= '`' . $column . '` = ?'; + + // only append colon if there are more entries + if($i < count($properties)-1){ + $columns .= ','; + } + + array_push($params, $entity->$getter()); + $i++; + } + + $sql = 'UPDATE `' . $this->tableName . '` SET ' . + $columns . ' WHERE `id` = ?'; + array_push($params, $id); + + $this->execute($sql, $params); + } + + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * @see findEntity + * @param string $sql the sql query + * @param array $params the parameters of the sql query + * @throws DoesNotExistException if the item does not exist + * @throws MultipleObjectsReturnedException if more than one item exist + * @return array the result as row + */ + protected function findOneQuery($sql, $params){ + $result = $this->execute($sql, $params); + $row = $result->fetchRow(); + + if($row === false || $row === null){ + throw new DoesNotExistException('No matching entry found'); + } + $row2 = $result->fetchRow(); + //MDB2 returns null, PDO and doctrine false when no row is available + if( ! ($row2 === false || $row2 === null )) { + throw new MultipleObjectsReturnedException('More than one result'); + } else { + return $row; + } + } + + + /** + * Runs an sql query + * @param string $sql the prepare string + * @param array $params the params which should replace the ? in the sql query + * @param int $limit the maximum number of rows + * @param int $offset from which row we want to start + * @return \PDOStatement the database query result + */ + protected function execute($sql, array $params=array(), $limit=null, $offset=null){ + $query = \OCP\DB::prepare($sql, $limit, $offset); + return $query->execute($params); + } + + /** + * Creates an entity from a row. Automatically determines the entity class + * from the current mapper name (MyEntityMapper -> MyEntity) + * @param array $row the row which should be converted to an entity + * @return Entity the entity + */ + protected function mapRowToEntity($row) { + $class = get_class($this); + $entityName = str_replace('Mapper', '', $class); + $entity = new $entityName(); + return $entity->fromRow($row); + } + + /** + * Runs a sql query and returns an array of entities + * @param string $sql the prepare string + * @param array $params the params which should replace the ? in the sql query + * @param int $limit the maximum number of rows + * @param int $offset from which row we want to start + * @return array all fetched entities + */ + protected function findEntities($sql, array $params=array(), $limit=null, $offset=null) { + $result = $this->execute($sql, $params, $limit, $offset); + + $class = get_class($this); + $collectionName = str_replace('Mapper', 'Collection', $class); + + $collection = new $collectionName(); + while($row = $result->fetchRow()){ + $entity = $this->mapRowToEntity($row); + $collection->add($entity); + } + return $collection; + } + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * @param string $sql the sql query + * @param array $params the parameters of the sql query + * @throws DoesNotExistException if the item does not exist + * @throws MultipleObjectsReturnedException if more than one item exist + * @return array the result as row + */ + protected function findEntity($sql, $params){ + $result = $this->execute($sql, $params); + $row = $result->fetchRow(); + + if($row === false){ + throw new DoesNotExistException('No matching entry found'); + } + $row2 = $result->fetchRow(); + //MDB2 returns null, PDO and doctrine false when no row is available + if( ! ($row2 === false || $row2 === null )) { + throw new MultipleObjectsReturnedException('More than one result'); + } else { + return $this->mapRowToEntity($row); + } + } +} diff --git a/db/multipleobjectsreturnedexception.php b/db/multipleobjectsreturnedexception.php new file mode 100644 index 000000000..9d177057a --- /dev/null +++ b/db/multipleobjectsreturnedexception.php @@ -0,0 +1,10 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +class MultipleObjectsReturnedException extends \Exception {} \ No newline at end of file diff --git a/db/object.php b/db/object.php new file mode 100644 index 000000000..dd49e4894 --- /dev/null +++ b/db/object.php @@ -0,0 +1,331 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \BadFunctionCallException; +use \DateTime; +use \OCA\Calendar\Sabre\VObject\Component\VCalendar; +use \OCA\Calendar\Sabre\VObject\Reader; +use \OCA\Calendar\Sabre\VObject\ParseException; +use \OCA\Calendar\Sabre\VObject\EofException; + +use \OCA\Calendar\Sabre\VObject\Component\VEvent; +use \OCA\Calendar\Sabre\VObject\Component\VJournal; +use \OCA\Calendar\Sabre\VObject\Component\VTodo; + +use \OCA\Calendar\Utility\ObjectUtility; +use \OCA\Calendar\Utility\SabreUtility; +use \OCA\Calendar\Utility\Utility; + +class Object extends Entity { + + public $id; + public $calendar; + public $objectURI; + public $etag; + public $ruds; + + public $vObject; + + private $objectName; + + /** + * @brief init Object object with data from db row + * @param array $fromRow + */ + public function __construct($fromRow=null){ + $this->addType('objectURI', 'string'); + $this->addType('etag', 'string'); + $this->addType('ruds', 'integer'); + + if(is_array($fromRow)) { + $this->fromRow($fromRow); + } + if($fromRow instanceof VCalendar) { + $this->fromVObject($fromRow); + } + } + + /** + * Maps the keys of the row array to the attributes + * @param array $row the row to map onto the entity + */ + public function fromRow(array $row) { + foreach($row as $key => $value){ + $prop = $this->columnToProperty($key); + if(property_exists($this, $prop)) { + if($value !== null && array_key_exists($prop, $this->fieldTypes)){ + settype($value, $this->fieldTypes[$prop]); + + } + $this->$prop = $value; + } + } + + if(array_key_exists('calendarData', $row) && trim($row['calendarData'] !== '')) { + $this->setCalendarData($row['calendarData']); + } + + return $this; + } + + /** + * set the calendarData + * @param string $data CalendarData + */ + public function setCalendarData($data) { + try { + $this->vobject = Reader::read($data); + $this->objectName = SabreUtility::getObjectName($this->vobject); + } catch(/* some */Exception $ex) { + + } + } + + /** + * @return array array of updated fields for update query + */ + public function getUpdatedFields() { + $updatedFields = parent::getUpdatedFields(); + + $properties = array( + 'objectURI', 'type', 'startDate', + 'endDate', 'calendarId', 'repeating', + 'summary', 'calendarData', 'lastModified', + ); + + foreach($properties as $property) { + $updatedFields[$property] = true; + } + + unset($updatedFields['calendar']); + unset($updatedFields['vObject']); + unset($updatedFields['objectName']); + + return $updatedFields; + } + + /** + * @brief take data from VObject and put into this Object object + * @param \Sabre\VObject\Component\VCalendar $vcalendar + * @return VCalendar Object + */ + public function fromVObject(VCalendar $vcalendar) { + $count = SabreUtility::countUniqueUIDs($vcalendar); + + if($count !== 1) { + $msg = 'Db\Object::fromVObject(): '; + $msg .= 'Multiple objects can\'t be stored in one resource.'; + throw new MultipleObjectsReturnedException($msg); + } + + $this->vobject = $vcalendar; + $this->objectName = SabreUtility::getObjectName($vcalendar); + } + + /** + * @brief get VObject from Calendar Object + * @return \Sabre\VObject\Component\VCalendar object + */ + public function getVObject() { + return $this->vobject; + } + + /** + * @brief expand an Array + * @param DateTime $start + * @param DateTime $end + * @return $this + */ + public function expand(DateTime $start, DateTime $end) { + try { + $this->vobject->expand($start, $end); + } catch(/* some */Exception $ex) { + //log debug msg; + } + + return $this; + } + + /** + * @brief set lastModified to now and update ETag + */ + public function touch() { + $now = new DateTime(); + $this->vobject->{$objectName}->{'LAST-MODIFIED'}->setDateTime($now); + $this->generateEtag(); + } + + /** + * @brief update Etag + */ + public function generateEtag() { + $etag = $this->getCalendarId(); + $etag .= $this->getObjectURI(); + $etag .= $this->getCalendarData(); + + $this->etag = md5($etag); + } + + /** + * @brief get type of stored object + * @return integer + */ + public function getType() { + return ObjectType::getTypeByString($this->objectName); + } + + /** + * @brief get startDate + * @return DateTime + */ + public function getStartDate() { + //TODO - USE UTILITY!! + //If recurrenceId is set, that's the actual start + //DTSTART has the value of the first object of recurring events + //This doesn't make any sense, but that's how it is in the standard + if(isset($this->vobject->{$objectName}->{'RECURRENCE-ID'})) { + return $this->vobject->{$objectName}->{'RECURRENCE-ID'}->getDateTime(); + } + + if(isset($this->vobject->{$objectName}->{'DTSTART'})) { + return $this->vobject->{$objectName}->{'DTSTART'}->getDateTime(); + } + + return null; + } + + /** + * @brief get endDate + * @return DateTime + */ + public function getEndDate() { + $objectName = $this->objectName; + + if(isset($this->vobject->{$objectName}->{'DTEND'})) { + return $this->vobject->{$objectName}->{'DTEND'}->getDateTime(); + } elseif(isset($this->vobject->{$objectName}->{'DURATION'})) { + $dtend = SabreUtility::getDTEnd($this->vobject->{$objectName}); + return $dtend->getDateTime(); + } else { + return $this->getStartDate(); + } + } + + /** + * @brief get whether or not object is repeating + * @return boolean + */ + public function getRepeating() { + $objectName = $this->objectName; + + if(isset($this->vobject->{$objectName}->{'RRULE'}) || + isset($this->vobject->{$objectName}->{'RDATE'}) || + isset($this->vobject->{$objectName}->{'RECURRENCE-ID'})) { + return true; + } + + return false; + } + + /** + * @brief get last occurence of repeating object + * @return mixed DateTime/null + */ + public function getLastOccurence() { + if($this->isRepeating() === false) { + return null; + } + + //THIS SHOULD DEFINITELY BE CACHED!! + + $lastOccurences = array(); + + if(isset($this->vobject->{$objectName}->{'RRULE'})) { + $rrule = $this->vobject->{$objectName}->{'RRULE'}; + //https://github.com/fruux/sabre-vobject/wiki/Sabre-VObject-Property-Recur + $parts = $rrule->getParts(); + if(!array_key_exists('COUNT', $parts) && array_key_exists('UNTIL', $parts)) { + return null; + } + //$lastOccurences[] = DateTime of last occurence + } + if(isset($this->vobject->{$objectName}->{'RDATE'})) { + //$lastOccurences[] = DateTime of last occurence + } + } + + /** + * @brief get summary of object + * @return string + */ + public function getSummary() { + $objectName = $this->objectName; + + if(isset($this->vobject->{$objectName}->{'SUMMARY'})) { + return $this->vobject->{$objectName}->{'SUMMARY'}->getValue(); + } + + return null; + } + + /** + * @brief get text/calendar representation of stored object + * @return integer + */ + public function getCalendarData() { + try { + return $this->vobject->serialize(); + } catch(/* some */Exception $ex) { + //log debug msg; + return null; + } + } + + /** + * @brief get last modified of object + * @return DateTime + */ + public function getLastModified() { + $objectName = $this->objectName; + + if(isset($this->vobject->{$objectName}->{'LAST-MODIFIED'})) { + return $this->vobject->{$objectName}->{'LAST-MODIFIED'}->getDateTime(); + } + + return null; + } + + /** + * @brief check if object is valid + * @return boolean + */ + public function isValid() { + $strings = array( + $this->objectURI, + $this->etag, + ); + + foreach($strings as $string) { + if(is_string($string) === false) { + return false; + } + if(trim($string) === '') { + return false; + } + } + + if(!($this->calendar instanceof Calendar)) { + return false; + } + + + $isVObjectValid = $this->vobject->validate(); + //TODO - finish implementation + } +} \ No newline at end of file diff --git a/db/objectcollection.php b/db/objectcollection.php new file mode 100644 index 000000000..8e357fc57 --- /dev/null +++ b/db/objectcollection.php @@ -0,0 +1,82 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \DateTime; + +class ObjectCollection extends Collection { + + /** + * @brief get a collection of entities within period + * @param DateTime $start + * @param DateTime $end + * @return ObjectCollection + */ + public function inPeriod(DateTime $start, DateTime $end) { + $objectsInPeriod = new ObjectCollection(); + + $this->iterate(function($object) use (&$objectsInPeriod) { + if($object->isRepeating() === true) { + $objectsInPeriod->add(clone $object); + } else { + + + + } + }); + + return $objectsInPeriod; + } + + /** + * @brief expand all entities of collection + * @param DateTime $start + * @param DateTime $end + * @return ObjectCollection + */ + public function expand(DateTime $start, DateTime $end) { + $expandedObjects = new ObjectCollection(); + + $this->iterate(function($object) use (&$expandedObjects) { + if($object->isRepeating() === true) { + + + + } else { + $expandedObjects->add(clone $object); + } + }); + + return $expandedObjects; + } + + /** + * @brief get a collection of all calendars owned by a certian user + * @param string userId of owner + * @return ObjectCollection + */ + public function ownedBy($userId) { + return $this->search('ownerId', $userId); + } + + /** + * @brief get a collection of all enabled calendars within collection + * @return ObjectCollection + */ + public function ofType($type) { + $objectsOfType = new ObjectCollection(); + + $this->iterate(function($object) use (&$objectsOfType, $type) { + if($object->getType() & $type) { + $collection->add(clone $object); + } + }); + + return $objectsOfType; + } +} \ No newline at end of file diff --git a/db/objectmapper.php b/db/objectmapper.php new file mode 100644 index 000000000..c3efe2257 --- /dev/null +++ b/db/objectmapper.php @@ -0,0 +1,122 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \OCP\AppFramework\IAppContainer; + +use \OCA\Calendar\Db\Object; + +class ObjectMapper extends Mapper { + /** + * @param API $api: Instance of the API abstraction layer + */ + public function __construct($api, $tablename = 'clndr_objcache'){ + parent::__construct($api, $tablename); + } + + /** + * Finds an item from user by it's uri + * @param string $uid + * @param integer $calendarId + * @throws DoesNotExistException: if the item does not exist + * @return the item + */ + public function find($uid, $calendarId){ + $sql = 'SELECT * FROM `'. $this->tableName . '` WHERE `uid` = ? AND `calendarid` = ?'; + $row = $this->findQuery($sql, array($uid, $calendarId)); + return new Object($row); + } + + /** + * Finds all Items of calendar $calendarId + * @param integer $calendarId + * @return array containing all items + */ + public function findAll($calendarId, $limit, $offset){ + $sql = 'SELECT * FROM `'. $this->tableName . '` WHERE `calendarid` = ?'; + return $this->findEntities($sql, array($calendarId), $limit, $offset); + } + + /** + * Finds all Items of calendar $calendarId of type $type + * @param integer $calendarId + * @param \OCA\Calendar\Db\ObjectType $type + * @return array containing all items of type $type + */ + public function findAllByType($calendarId, $type, $limit, $offset) { + $sql = 'SELECT * FROM `'. $this->tableName . '` WHERE `calendarid` = ? AND `type` = ?'; + return $this->findEntities($sql, array($calendarId, $type), $limit, $offset); + } + + /** + * Finds all Items of calendar $calendarId from $start to $end + * @param integer $calendarId + * @param DateTime $start + * @param DateTime $end + * @return array containing all items of type $type + */ + public function findAllInPeriod($calendarId, $start, $end, $limit, $offset) { + $utcStart = $this->getUTC($start); + $utcEnd = $this->getUTC($end); + $sql = 'SELECT * FROM `'. $this->tableName . '` WHERE `calendarid` = ?'; + $sql .= 'AND ((`startdate` >= ? AND `startdate` <= ?)'; + $sql .= ' OR (`enddate` >= ? AND `enddate` <= ?)'; + $sql .= ' OR (`startdate` <= ? AND `enddate` >= ?)'; + $sql .= ' OR (`lastoccurence` >= ? AND `startdate` <= ? AND `repeating` = 1))'; + return $this->findEntities($sql, array($calendarId, + $utcStart, $utcEnd, + $utcStart, $utcEnd, + $utcStart, $utcEnd, + $utcStart, $utcEnd), + $limit, $offset); + } + + /** + * Finds all Items of calendar $calendarId of type $type in period from $start to $end + * @param integer $calendarId + * @param DateTime $start + * @param DateTime $end + * @param \OCA\Calendar\Db\ObjectType $type + * @return array containing all items of type $type + */ + public function findAllByTypeInPeriod($calendarId, $start, $end, $type, $limit, $offset) { + $utcStart = $this->getUTC($start); + $utcEnd = $this->getUTC($end); + $sql = 'SELECT * FROM `'. $this->tableName . '` WHERE `calendarid` = ? AND `type` = ?'; + $sql .= 'AND ((`startdate` >= ? AND `startdate` <= ?)'; + $sql .= ' OR (`enddate` >= ? AND `enddate` <= ?)'; + $sql .= ' OR (`startdate` <= ? AND `enddate` >= ?)'; + $sql .= ' OR (`lastoccurence` >= ? AND `startdate` <= ? AND `repeating` = 1))'; + return $this->findEntities($sql, array($calendarId, $type, + $utcStart, $utcEnd, + $utcStart, $utcEnd, + $utcStart, $utcEnd, + $utcStart, $utcEnd), + $limit, $offset); + } + + /** + * Deletes all objects of calendar $calendarid + * @param integer $calendarId + */ + public function deleteAll($calendarId) { + $sql = 'DELETE FROM `' . $this->getTableName() . '` '; + $sql .= 'WHERE `calendarid` = ?'; + + $this->execute($sql, array($calendarId)); + } + + /** + * get UTC from a datetime object + * @param DateTime $datetime + * @return string + */ + private function getUTC($datetime){ + return date('Y-m-d H:i:s', $datetime->format('U')); + } +} \ No newline at end of file diff --git a/db/objecttype.php b/db/objecttype.php new file mode 100644 index 000000000..83950c138 --- /dev/null +++ b/db/objecttype.php @@ -0,0 +1,114 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +class ObjectType { + const EVENT = 1; + const JOURNAL = 2; + const TODO = 4; + const ALL = 7; + + public static function getAsString($type) { + $types = array(); + + if($type & self::EVENT) { + $types[] = 'VEVENT'; + } + if($type & self::JOURNAL) { + $types[] = 'VJOURNAL'; + } + if($type & self::TODO) { + $types[] = 'VTODO'; + } + + $string = implode(',', $types); + return $string; + } + + public static function getAsReadableString($type) { + $types = array(); + + if($type & self::EVENT) { + $types[] = 'events'; + } + if($type & self::JOURNAL) { + $types[] = 'journals'; + } + if($type & self::TODO) { + $types[] = 'todos'; + } + + $string = implode(', ', $types); + return $string; + } + + public static function getTypeByString($string) { + $type = 0; + + switch($string) { + case 'VEVENT': + $type = self::EVENT; + break; + + case 'VJOURNAL': + $type = self::JOURNAL; + break; + + case 'VTODO': + $type = self::TODO; + break; + + default: + break; + } + + return $type; + } + + public static function getTypesByString($string) { + $types = 0; + + $string = strtoupper($string); + if(substr_count($string, 'VEVENT')) { + $types += self::EVENT; + } + if(substr_count($string, 'VJOURNAL')) { + $types += self::JOURNAL; + } + if(substr_count($string, 'VTODO')) { + $types += self::TODO; + } + } + + public static function getTypeBySabreClass($class) { + $type = 0; + + if(is_string($class)) { + $class = get_class($class); + } + + switch($string) { + case 'OCA\Calendar\Sabre\VObject\Component\VEvent': + $type = self::EVENT; + break; + + case 'OCA\Calendar\Sabre\VObject\Component\VJournal': + $type = self::JOURNAL; + break; + + case 'OCA\Calendar\Sabre\VObject\Component\VTodo': + $type = self::TODO; + break; + + default: + break; + } + + return $type; + } +} \ No newline at end of file diff --git a/db/permissions.php b/db/permissions.php new file mode 100644 index 000000000..17370d3f4 --- /dev/null +++ b/db/permissions.php @@ -0,0 +1,17 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +class Permissions { + const CREATE = 4; + const READ = 1; + const UPDATE = 2; + const DELETE = 8; + const SHARE = 16; + const ALL = 31; +} \ No newline at end of file diff --git a/db/timezone.php b/db/timezone.php new file mode 100644 index 000000000..e8b581141 --- /dev/null +++ b/db/timezone.php @@ -0,0 +1,64 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +use \OCA\Calendar\Sabre\VObject\Component\VCalendar; + +use \OCA\Calendar\Utility\Utility; + +class Timezone extends Entity { + + protected $timezone; + + /** + * @brief init Timezone object with timezone name + * @param string $timezone + */ + public function __construct($timezone='UTC') { + if(Utility::isTimezoneSupported($timezone)) { + $this->timezone = new \DateTimeZone($timezone); + } else { + + $msg = ''; + throw new /* some */Exception($msg); + } + } + + + /** + * @brief check if object is valid + * @return boolean + */ + public function isValid() { + return true; + } + + + /** + * @brief take data from VObject and put into this Timezone object + * @param \Sabre\VObject\Component\VCalendar $vcalendar + * @return $this + */ + public function fromVObject(VCalendar $vcalendar) { + //TODO implement + } + + + /** + * @brief get VObject from Calendar Object + * @return \Sabre\VObject\Component\VCalendar object + */ + public function getVObject() { + //TODO implement + } + + + public function __toString() { + return $this->timezone->getName(); + } +} \ No newline at end of file diff --git a/db/timezonecollection.php b/db/timezonecollection.php new file mode 100644 index 000000000..3e6393610 --- /dev/null +++ b/db/timezonecollection.php @@ -0,0 +1,10 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Db; + +class TimezoneCollection extends Collection {} \ No newline at end of file diff --git a/http/ics/calendar.php b/http/ics/calendar.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/ics/calendarcollection.php b/http/ics/calendarcollection.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/ics/calendarreader.php b/http/ics/calendarreader.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/ics/ics.php b/http/ics/ics.php new file mode 100644 index 000000000..7cff2017c --- /dev/null +++ b/http/ics/ics.php @@ -0,0 +1,41 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\ICS; + +use \OCA\Calendar\Http\IResponse; + +abstract class ICS implements IResponse { + + protected $object; + + /** + * @brief Constructor + */ + public function __construct($object) { + $this->object = $object; + } + + /** + * @brief get object JSONObject was initialized with. + */ + protected function getObject() { + return $this->object; + } + + /** + * @brief get mimetype of serialized output + */ + public function getMimeType() { + return 'text/calendar'; + } + + /** + * @brief get ics-encoded string containing all information + */ + abstract public function serialize(); +} \ No newline at end of file diff --git a/http/ics/icscollection.php b/http/ics/icscollection.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/ics/icsreader.php b/http/ics/icsreader.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/ics/object.php b/http/ics/object.php new file mode 100644 index 000000000..1d825f7d1 --- /dev/null +++ b/http/ics/object.php @@ -0,0 +1,31 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\ICS; + +use OCA\Calendar\Sabre\VObject\Reader; +use OCA\Calendar\Sabre\VObject\Component\VEvent; +use OCA\Calendar\Sabre\VObject\Component\VJournal; +use OCA\Calendar\Sabre\VObject\Component\VTodo; + +use \OCA\Calendar\Db\Object; + +class ICSObject extends ICS { + + /** + * @brief get json-encoded string containing all information + */ + public function serialize($convenience=true) { + $vcalendar = $this->object->getVObject(); + + if($convenience === true) { + JSONUtility::addConvenience($vcalendar); + } + + return $vcalendar->serialize(); + } +} \ No newline at end of file diff --git a/http/ics/objectcollection.php b/http/ics/objectcollection.php new file mode 100644 index 000000000..07a78bfd7 --- /dev/null +++ b/http/ics/objectcollection.php @@ -0,0 +1,24 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\ICS; + +class ICSObjectCollection extends ICSCollection { + + /** + * @brief get json-encoded string containing all information + */ + public function serialize($convenience=true) { + $vcalendar = $this->collection->getVObject(); + + if($convenience === true) { + JSONUtility::addConvenience($vcalendar); + } + + return $vcalendar->serialize(); + } +} \ No newline at end of file diff --git a/http/ics/objectreader.php b/http/ics/objectreader.php new file mode 100644 index 000000000..dd82b853b --- /dev/null +++ b/http/ics/objectreader.php @@ -0,0 +1,58 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\ICS; + +use OCA\Calendar\Sabre\VObject\Reader; + +class ICSObjectReader extends ICSReader { + + public function parse() { + try{ + $this->fixData(); + + $stream = fopen('php://memory','r+'); + fwrite($stream, $this->getData()); + rewind($stream); + + //TODO - does the vobject splitter support json-encoded calendar data??????? + $vcalendar = new ICalendar($stream); + + $objectCollection = new ObjectCollection(); + + while($vobject = $vcalendar->next()) { + $object = new Object(); + $object->fromVObject($vcalendar); + $objectCollection->add($object); + } + + if($objectCollection->count() === 1) { + $this->setObject($objectCollection->reset()->current()); + } else { + $this->setObject($objectCollection); + } + + return $this; + } catch(Exception $e /* What exception is being thrown??? */) { + throw new JSONObjectReaderException($ex->getMessage()); + } + } + + protected function fixData() { + $data = $this->getData(); + + //fix malformed timestamp in some google calendar events + //originally contributed by nezzi@github + $data = str_replace('CREATED:00001231T000000Z', 'CREATED:19700101T000000Z', $data); + + //add some more fixes over time + + $this->setData($data); + } +} + +class JSONObjectReaderException extends Exception{} \ No newline at end of file diff --git a/http/ics/timezone.php b/http/ics/timezone.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/ics/timezonecollection.php b/http/ics/timezonecollection.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/ics/timezonereader.php b/http/ics/timezonereader.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/iresponse.php b/http/iresponse.php new file mode 100644 index 000000000..57fd2ab73 --- /dev/null +++ b/http/iresponse.php @@ -0,0 +1,16 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http; + +interface IResponse { + + public function getMimeType(); + + public function serialize(); + +} \ No newline at end of file diff --git a/http/json/calendar.php b/http/json/calendar.php new file mode 100644 index 000000000..eb4010256 --- /dev/null +++ b/http/json/calendar.php @@ -0,0 +1,147 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + * + * Example output: + * ```json + * { + * "displayname" : "Work", + * "calendarURI" : "local-work", + * "owner" : { + * "userid" : "developer42", + * "displayname" : "developer42" + * }, + * "ctag" : 0, + * "url" : "https://owncloud/index.php/apps/calendar/calendars/local-work", + * "color" : "#000000", + * "order" : 0, + * "enabled" : true, + * "components" : { + * "vevent" : true, + * "vjournal" : false, + * "vtodo" : true + * }, + * "timezone" : {}, //see JSONTIMEZONE + * "user" : { + * "userid" : "developer42", + * "displayname" : "developer42" + * }, + * "cruds" : { + * "create" : true, + * "update" : true, + * "delete" : true, + * "code" : 31, + * "read" : true, + * "share" : true + * } + * } + * ``` + */ +namespace OCA\Calendar\Http\JSON; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\ObjectType; +use \OCA\Calendar\Db\Permissions; + +use \OCA\Calendar\Utility\CalendarUtility; +use \OCA\Calendar\Utility\JSONUtility; + +class JSONCalendar extends JSON { + + private $jsonArray; + + /** + * @brief get mimetype of serialized output + */ + public function getMimeType() { + return 'application/json'; + } + + + public function serialize($convenience=true) { + $this->jsonArray = array(); + + $properties = get_object_vars($this->object); + + foreach($properties as $key => $key) { + $propertyGetter = 'get' . ucfirst($key); + $key = strtolower($key); + $value = $this->object->{$propertyGetter}(); + + switch($key) { + case 'color': + case 'displayname': + case 'timezone': + $this->jsonArray[$key] = (string) $value; + break; + + case 'ctag': + case 'order': + $this->jsonArray[$key] = (int) $value; + break; + + case 'enabled': + $this->jsonArray[$key] = (bool) $value; + break; + + case 'components': + $this->jsonArray[$key] = JSONUtility::getComponents($value); + break; + + case 'cruds': + $this->jsonArray[$key] = JSONUtility::getCruds($value); + break; + + case 'ownerId': + case 'userId': + $key = substr($key, 0, (strlen($key) - 2)); + $this->jsonArray[$key] = JSONUtility::getUserInformation($value); + break; + + //blacklist + case 'id': + case 'backend': + case 'uri': + break; + + default: + $this->jsonArray[$key] = $value; + break; + + } + } + + $this->setCalendarURI(); + $this->setCalendarURL(); + + return $this->jsonArray; + } + + /** + * @brief set public calendar uri + */ + private function setCalendarURI() { + $backend = $this->object->getBackend(); + $calendarURI = $this->object->getUri(); + + $calendarURI = CalendarUtility::getURI($backend, $calendarURI); + + $this->jsonArray['calendarURI'] = $calendarURI; + } + + /** + * @brief set api url to calendar + */ + private function setCalendarURL() { + $calendarURI = $this->jsonArray['calendarURI']; + + //TODO - fix me + //$calendarURL = JSONUtility::getURL($calendarURI); + $calendarURL = ''; + + $this->jsonArray['url'] = $calendarURL; + } +} \ No newline at end of file diff --git a/http/json/calendarcollection.php b/http/json/calendarcollection.php new file mode 100644 index 000000000..8924b2e1f --- /dev/null +++ b/http/json/calendarcollection.php @@ -0,0 +1,45 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\JSON; + +use \OCA\Calendar\Db\Calendar; + +class JSONCalendarCollection extends JSONCollection { + + /** + * @brief get mimetype of serialized output + */ + public function getMimeType() { + return 'application/json'; + } + + + /** + * @brief get json-encoded string containing all information + */ + public function serialize() { + $jsonArray = array(); + + $this->object->iterate(function(&$object) use (&$jsonArray) { + try { + if($object instanceof Calendar) { + $jsonCalendar = new JSONCalendar($object); + $jsonArray[] = $jsonCalendar->serialize(); + } + if($object instanceof JSONCalendar) { + $jsonArray[] = $object->serialize(); + } + } catch (JSONException $ex) { + //TODO - log error msg + return; + } + }); + + return $jsonArray; + } +} \ No newline at end of file diff --git a/http/json/calendarreader.php b/http/json/calendarreader.php new file mode 100644 index 000000000..2f740a0d5 --- /dev/null +++ b/http/json/calendarreader.php @@ -0,0 +1,130 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\JSON; + +use \OCA\Calendar\Db\Calendar; +use \OCA\Calendar\Db\ObjectType; +use \OCA\Calendar\Db\Permissions; + +use \OCA\Calendar\Utility\CalendarUtility; +use \OCA\Calendar\Utility\JSONUtility; + +class JSONCalendarReader extends JSONReader{ + + /** + * @brief parse jsoncalendar + */ + public function parse() { + try{ + $data = $this->getData(); + $isCollection = false; + + if(array_key_exists(0, $data) === true && is_array($data[0]) === true) { + $isCollection = true; + } + + if($isCollection === false) { + try { + $object = $this->parseSingleEntry($data); + $this->setObject($object); + } catch(/* some */Exception $ex) { + //TODO - log error msg + return; + } + } else { + $collection = new CalendarCollection(); + + foreach($data as $entry) { + try { + $object = $this->parseSingleEntry($data); + $collection->add($object); + } catch(/* some */Exception $ex) { + //TODO - log some error msg + continue; + } + } + + $this->setObject($collection); + } + } catch(Exception $ex /* What exception is being thrown??? */) { + throw new JSONCalendarReaderException($ex->getMessage()); + } + } + + /** + * @brief overwrite values that should not be set by user with null + */ + public function sanitize() { + //make sure object exists + $this->getObject(); + + $sanitize = array( + 'userId', + 'ownerId', + 'cruds', + 'ctag', + ); + + parent::nullProperties($sanitize); + return $this; + } + + private function parseSingleEntry($data) { + $calendar = new Calendar(); + + foreach($data as $key => $value) { + $propertySetter = 'set' . ucfirst($key); + + switch($key) { + case 'color': + case 'displayname': + case 'timezone': + $calendar->{$propertySetter}((string) $value); + break; + + case 'ctag': + case 'order': + $calendar->{$propertySetter}((int) $value); + break; + + case 'enabled': + $calendar->{$propertySetter}((bool) $value); + break; + + case 'components': + $calendar->{$propertySetter}(JSONUtility::parseComponents($value)); + break; + + case 'cruds': + $calendar->{$propertySetter}(JSONUtility::parseCruds($value)); + break; + + case 'owner': + case 'user': + $propertySetter .= 'Id'; + $calendar->{$propertySetter}(JSONUtility::parseUserInformation($value)); + break; + + case 'calendarURI': + $calendar->setBackend(JSONUtility::parseCalendarURIForBackend($value)); + $calendar->setUri(JSONUtility::parseCalendarURIForURI($value)); + + //blacklist: + case 'url': + break; + + default: + break; + } + } + + return $calendar; + } +} + +class JSONCalendarReaderException extends \Exception{} \ No newline at end of file diff --git a/http/json/json.php b/http/json/json.php new file mode 100644 index 000000000..fcc3f69f5 --- /dev/null +++ b/http/json/json.php @@ -0,0 +1,50 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\JSON; + +use \OCA\Calendar\Db\Entity; +use \OCA\Calendar\Http\IResponse; + +abstract class JSON implements IResponse { + + protected $object; + protected $vobject; + + /** + * @brief Constructor + */ + public function __construct(Entity $object) { + $this->object = $object; + try { + $this->vobject = $object->getVObject(); + } catch(/* some */Exception $ex) { + + } + } + + /** + * @brief get object JSONObject was initialized with. + */ + protected function getObject() { + return $this->object; + } + + /** + * @brief get mimetype of serialized output + */ + public function getMimeType() { + return 'application/calendar+json'; + } + + /** + * @brief get json-encoded string containing all information + */ + abstract public function serialize(); +} + +class JSONException extends \Exception {} \ No newline at end of file diff --git a/http/json/jsoncollection.php b/http/json/jsoncollection.php new file mode 100644 index 000000000..c11dd2aa5 --- /dev/null +++ b/http/json/jsoncollection.php @@ -0,0 +1,21 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\JSON; + +use \OCA\Calendar\Db\Collection; +use \OCA\Calendar\Http\IResponse; + +abstract class JSONCollection extends JSON { + + /** + * @brief Constructor + */ + public function __construct(Collection $object) { + $this->object = $object; + } +} \ No newline at end of file diff --git a/http/json/jsonreader.php b/http/json/jsonreader.php new file mode 100644 index 000000000..3da0cce07 --- /dev/null +++ b/http/json/jsonreader.php @@ -0,0 +1,132 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\JSON; + +use \OCA\Calendar\Db\Collection; + +abstract class JSONReader { + + protected $data; + protected $object; + + /** + * @brief Constructor + */ + public function __construct($json=null) { + if($json !== null) { + $this->setData($json); + } + } + + /** + * @brief get data + * @return mixed + */ + protected function getData() { + return $this->data; + } + + /** + * @brief set data + */ + public function setData($json) { + $this->object = null; + + if(is_array($json)) { + $this->data = $json; + return $this; + } + + if(is_string($json)) { + if(trim($json) === '') { + $msg = 'JSONReader::setData(): User Error: '; + $msg .= 'Given string is empty'; + throw new JSONReaderException($msg); + } + + $data = json_decode($json, true); + if(!is_string($data)) { + $msg = 'JSONReader::setData(): User Error: '; + $msg .= 'Could not parse given (json) string!'; + throw new JSONReaderException($msg); + } + + $this->data = $data; + return $this; + } + + $msg = 'JSONReader::setData(): User Error: '; + $msg .= 'Could not recognise given data format!'; + throw new JSONReaderException($msg); + } + + /** + * @brief get object created from reader + */ + public function getObject() { + if($this->getData() === null) { + $msg = 'JSONReader::getObject(): Internal Error: '; + $msg .= 'getObject may not be called before any data was set!'; + throw new JSONReaderException($msg); + } + + if($this->object === null) { + $this->parse(); + } + + return $this->object; + } + + /** + * @brief set object + */ + protected function setObject($object) { + if(($object instanceof Entity) || + ($object instanceof Collection)) { + $this->object = $object; + return $this; + } + + return null; + } + + /** + * @brief get if reader will return collection + */ + public function isCollection() { + if($this->object === null) { + $this->parse(); + } + + return ($this->object instanceof Collection); + } + + /** + * @brief null properties + * @param array of strings + * string should represent key + */ + protected function nullProperties($properties) { + $isCollection = $this->isCollection(); + + foreach($properties as $property) { + if($isCollection) { + $this->object->setProperty($property, null); + } else { + $setter = 'set' . ucfirst($property); + $this->object->{$setter}(null); + } + } + + return $this; + } + + abstract public function parse(); +} + +class JSONReaderException extends Exception {} \ No newline at end of file diff --git a/http/json/object.php b/http/json/object.php new file mode 100644 index 000000000..bd166b758 --- /dev/null +++ b/http/json/object.php @@ -0,0 +1,24 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\JSON; + +class JSONObject extends JSON { + + /** + * @brief get json-encoded string containing all information + */ + public function serialize($convenience=true) { + $vcalendar = $this->object->getVObject(); + + if($convenience === true) { + JSONUtility::addConvenience($vcalendar); + } + + return $vcalendar->jsonSerialize(); + } +} \ No newline at end of file diff --git a/http/json/objectcollection.php b/http/json/objectcollection.php new file mode 100644 index 000000000..aa673ed41 --- /dev/null +++ b/http/json/objectcollection.php @@ -0,0 +1,15 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\JSON; + +class JSONObjectCollection extends JSONObject { + + public function extend(\DateTime $start, \DateTime $end) { + $this->vobject->extend($start, $end); + } +} \ No newline at end of file diff --git a/http/json/objectreader.php b/http/json/objectreader.php new file mode 100644 index 000000000..6b5501381 --- /dev/null +++ b/http/json/objectreader.php @@ -0,0 +1,62 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http\JSON; + +use OCA\Calendar\Db\Object; +use OCA\Calendar\Db\ObjectCollection; + +use OCA\Calendar\VObject\Splitter\ICalendar; + +class JSONObjectReader { + + public function parse() { + try { + $this->fixData(); + + $stream = fopen('php://memory','r+'); + fwrite($stream, $this->getData()); + rewind($stream); + + //TODO - does the vobject splitter support json-encoded calendar data??????? + $vcalendar = new ICalendar($stream); + + $objectCollection = new ObjectCollection(); + + while($vobject = $vcalendar->next()) { + $object = new Object(); + $object->fromVObject($vcalendar); + $objectCollection->add($object); + } + + if($objectCollection->count() === 1) { + $this->setObject($objectCollection->reset()->current()); + } else { + $this->setObject($objectCollection); + } + + return $this; + } catch(/* some */Exception $e) { + throw new JSONObjectReaderException($ex->getMessage()); + } + } + + protected function fixData() { + $data = $this->getData(); + + //fix malformed timestamp in some google calendar events + //originally contributed by nezzi@github + //TODO - convert to json representation of created + $data = str_replace('CREATED:00001231T000000Z', 'CREATED:19700101T000000Z', $data); + + //add some more fixes over time + + $this->setData($data); + } +} + +class JSONObjectReaderException extends Exception{} \ No newline at end of file diff --git a/http/json/timezone.php b/http/json/timezone.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/json/timezonecollection.php b/http/json/timezonecollection.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/json/timezonereader.php b/http/json/timezonereader.php new file mode 100644 index 000000000..e69de29bb diff --git a/http/jsonresponse.php b/http/jsonresponse.php new file mode 100644 index 000000000..d9372cec4 --- /dev/null +++ b/http/jsonresponse.php @@ -0,0 +1,54 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Http; + +use \OCP\AppFramework\Http\Response; +use \OCP\AppFramework\Http; +use \OCA\Calendar\Http\IResponse; + +class JSONResponse extends Response { + + protected $data; + + /** + * @param array|object $data the object or array that should be transformed + * @param int $statusCode the Http status code, defaults to 200 + */ + public function __construct(&$data=null, $statusCode=null) { + $this->data = $data; + + if($statusCode === null) { + if($data instanceof IResponse) { + $statusCode = Http::STATUS_OK; + } else { + $statusCode = Http::STATUS_NO_CONTENT; + } + } + + $this->setStatus($statusCode); + $this->addHeader('X-Content-Type-Options', 'nosniff'); + $this->addHeader('Content-type', 'application/json; charset=utf-8'); + } + + /** + * Returns the rendered json + * @return string the rendered json + */ + public function render(){ + if($this->data instanceof IResponse) { + $data = $this->data->serialize(); + + if(is_array($data)) { + return json_encode($data); + } else { + return $data; + } + } + return ''; + } +} \ No newline at end of file diff --git a/img/calendar.png b/img/calendar.png new file mode 100644 index 000000000..954ecbc61 Binary files /dev/null and b/img/calendar.png differ diff --git a/img/calendar.svg b/img/calendar.svg new file mode 100644 index 000000000..8d70624b1 --- /dev/null +++ b/img/calendar.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/l10n/af_ZA.php b/l10n/af_ZA.php new file mode 100644 index 000000000..1d6e13909 --- /dev/null +++ b/l10n/af_ZA.php @@ -0,0 +1,5 @@ + "Persoonlik", +"Settings" => "Instellings", +"Advanced" => "Gevorderd" +); diff --git a/l10n/ar.php b/l10n/ar.php new file mode 100644 index 000000000..9c1c2061a --- /dev/null +++ b/l10n/ar.php @@ -0,0 +1,215 @@ + "ليس جميع الجداول الزمنيه محفوضه مؤقة", +"Everything seems to be completely cached" => "كل شيء محفوض مؤقة", +"No calendars found." => "لم يتم العثور على جدول الزمني", +"No events found." => "لم يتم العثور على احداث", +"Wrong calendar" => "جدول زمني خاطئ", +"You do not have the permissions to edit this event." => "ليس لديك الصلاحية لتعديل هذا التقويم.", +"The file contained either no events or all events are already saved in your calendar." => "الملف إما ليس به أحداث أو قد تكون كل الأحداث حفظت فى التقويم الخاص بك.", +"events has been saved in the new calendar" => "تم حفظ الأحداث فى التقويم الجديد", +"Import failed" => "لقد فشل الإستيراد", +"events has been saved in your calendar" => "تم حفظ الأحداث فى التقويم الخاص بك", +"New Timezone:" => "التوقيت الجديد", +"Timezone changed" => "تم تغيير المنطقة الزمنية", +"Invalid request" => "طلب غير مفهوم", +"Calendar" => "الجدول الزمني", +"Deletion failed" => "فشل الحذف", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ي ش [ع]{-[ي] ش ع}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ي ش [ع] س:د{-[ي ش ع] س:د}", +"user" => "مستخدم", +"group" => "مجموعة", +"Editable" => "يمكن تعديله", +"Shareable" => "قابل للمشاركة", +"Deletable" => "قابل للحذف", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "ddd M/d", +"MMMM yyyy" => "ddd M/d", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "الاحد", +"Monday" => "الأثنين", +"Tuesday" => "الثلاثاء", +"Wednesday" => "الاربعاء", +"Thursday" => "الخميس", +"Friday" => "الجمعه", +"Saturday" => "السبت", +"Sun." => "أحد", +"Mon." => "أثن.", +"Tue." => "ثلا.", +"Wed." => "أرب.", +"Thu." => "خمي.", +"Fri." => "جمع.", +"Sat." => "سبت", +"January" => "كانون الثاني", +"February" => "شباط", +"March" => "آذار", +"April" => "نيسان", +"May" => "أيار", +"June" => "حزيران", +"July" => "تموز", +"August" => "آب", +"September" => "أيلول", +"October" => "تشرين الاول", +"November" => "تشرين الثاني", +"December" => "كانون الاول", +"Jan." => "ك2", +"Feb." => "شبا.", +"Mar." => "آذا.", +"Apr." => "نيس.", +"May." => "أيا.", +"Jun." => "حزي.", +"Jul." => "تمو.", +"Aug." => "آب", +"Sep." => "أيل.", +"Oct." => "ت1", +"Nov." => "ت2", +"Dec." => "ك1", +"All day" => "كل اليوم ", +"New Calendar" => "جدول زمني جديد", +"Missing or invalid fields" => "خانات غير صالحة أو خالية من المعلومات", +"Title" => "عنوان", +"From Date" => "من تاريخ", +"From Time" => "إلى تاريخ", +"To Date" => "إلى يوم", +"To Time" => "إلى وقت", +"The event ends before it starts" => "هذا الحدث ينتهي قبل أن يبدأ", +"There was a database fail" => "خطأ في قاعدة البيانات", +"Birthday" => "عيد ميلاد", +"Business" => "عمل", +"Call" => "إتصال", +"Clients" => "الزبائن", +"Deliverer" => "المرسل", +"Holidays" => "عطلة", +"Ideas" => "أفكار", +"Journey" => "رحلة", +"Jubilee" => "يوبيل", +"Meeting" => "إجتماع", +"Other" => "شيء آخر", +"Personal" => "شخصي", +"Projects" => "مشاريع", +"Questions" => "اسئلة", +"Work" => "العمل", +"by" => "من قبل", +"unnamed" => "غير مسمى", +"You do not have the permissions to update this calendar." => "ليس لديك الصلاحية لتحديث هذا التقويم.", +"You do not have the permissions to delete this calendar." => "ليس لديك الصلاحية لحذف هذا التقويم.", +"You do not have the permissions to add to this calendar." => "ليس لديك الصلاحية لإضافة هذا التقويم.", +"You do not have the permissions to add events to this calendar." => "ليس لديك الصلاحية لإضافة أحداث لهذا التقويم.", +"You do not have the permissions to delete this event." => "ليس لديك الصلاحية لحذف هذا الحدث.", +"Busy" => "مشغول", +"Public" => "علني", +"Private" => "خاص", +"Confidential" => "سرّي", +"Does not repeat" => "لا يعاد", +"Daily" => "يومي", +"Weekly" => "أسبوعي", +"Every Weekday" => "كل نهاية الأسبوع", +"Bi-Weekly" => "كل اسبوعين", +"Monthly" => "شهري", +"Yearly" => "سنوي", +"never" => "بتاتا", +"by occurrences" => "حسب تسلسل الحدوث", +"by date" => "حسب التاريخ", +"by monthday" => "حسب يوم الشهر", +"by weekday" => "حسب يوم الاسبوع", +"events week of month" => "الاحداث باسبوع الشهر", +"first" => "أول", +"second" => "ثاني", +"third" => "ثالث", +"fourth" => "رابع", +"fifth" => "خامس", +"last" => "أخير", +"by events date" => "حسب تاريخ الحدث", +"by yearday(s)" => "حسب يوم السنه", +"by weeknumber(s)" => "حسب رقم الاسبوع", +"by day and month" => "حسب اليوم و الشهر", +"Date" => "تاريخ", +"Cal." => "تقويم", +"Week" => "إسبوع", +"Month" => "شهر", +"List" => "قائمة", +"Today" => "اليوم", +"Settings" => "تعديلات", +"Your calendars" => "جداولك الزمنيه", +"CalDav Link" => "وصلة CalDav", +"Share Calendar" => "شارك الجدول الزمني", +"Download" => "تحميل", +"Edit" => "تعديل", +"Delete" => "حذف", +"New calendar" => "جدول زمني جديد", +"Edit calendar" => "عادل الجدول الزمني", +"Displayname" => "الاسم المرئي", +"Active" => "حالي", +"Calendar color" => "لون الجدول الزمني", +"Save" => "إحفظ", +"Submit" => "أرسل", +"Cancel" => "إلغاء", +"Edit an event" => "عادل حدث", +"Export" => "تصدير المعلومات", +"Eventinfo" => "تفاصيل الحدث", +"Repeating" => "يعاد", +"Alarm" => "تنبيه", +"Attendees" => "الحضور", +"Share" => "شارك", +"Title of the Event" => "عنوان الحدث", +"Category" => "فئة", +"Separate categories with commas" => "افصل الفئات بالفواصل", +"Edit categories" => "عدل الفئات", +"Access Class" => "فئة السماح بالدخول", +"All Day Event" => "حدث في يوم كامل", +"From" => "من", +"To" => "إلى", +"Advanced options" => "خيارات متقدمة", +"Location" => "مكان", +"Location of the Event" => "مكان الحدث", +"Description" => "مواصفات", +"Description of the Event" => "وصف الحدث", +"Repeat" => "إعادة", +"Advanced" => "تعديلات متقدمه", +"Select weekdays" => "اختر ايام الاسبوع", +"Select days" => "اختر الايام", +"and the events day of year." => "و التواريخ حسب يوم السنه.", +"and the events day of month." => "و الاحداث حسب يوم الشهر.", +"Select months" => "اختر الاشهر", +"Select weeks" => "اختر الاسابيع", +"and the events week of year." => "و الاحداث حسب اسبوع السنه", +"Interval" => "المده الفاصله", +"End" => "نهايه", +"occurrences" => "الاحداث", +"create a new calendar" => "انشاء جدول زمني جديد", +"Import a calendar file" => "أدخل ملف التقويم", +"Please choose a calendar" => "من فضلك اختر التقويم", +"Name of new calendar" => "أسم الجدول الزمني الجديد", +"Take an available name!" => "خذ اسم متاح!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "تقويم مع هذا الاسم موجود مسبقا. إذا استمريت على أية حال، سوف يتم دمج هذه التقويمات.", +"Remove all events from the selected calendar" => "الغى كافة الأحداث من التقويم المحدد", +"Import" => "إدخال", +"Close Dialog" => "أغلق الحوار", +"Create a new event" => "إضافة حدث جديد", +"Share with:" => "شارك ب:", +"Shared with" => "تمت المشاركة مع", +"Unshare" => "إلغاء مشاركة", +"Nobody" => "لا أحد", +"Shared via calendar" => "تمت المشاركة عبر التقويم", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "ملاحظة: الإجراءات الجارية على الأحداث التى تمت المشاركة بها عبر التقويم سيؤثر على المشاركة الخاصة بالتقويم بأكمله.", +"View an event" => "شاهد الحدث", +"No categories selected" => "لم يتم اختيار الفئات", +"of" => "من", +"at" => "في", +"General" => "عام", +"Timezone" => "المنطقة الزمنية", +"Update timezone automatically" => "حدث التوقيت تلقائياً", +"Time format" => "صيغة الوقت", +"24h" => "24 ساعة", +"12h" => "12 ساعة", +"Start week on" => "بداية الأسبوع", +"Cache" => "ذاكرة التخزين المؤقت", +"Clear cache for repeating events" => "امسح ذاكرة التخزين المؤقت لتكرار الأحداث", +"URLs" => "عناوين المواقع", +"Calendar CalDAV syncing addresses" => "عنواين التقويم عن بعد المزامنة", +"more info" => "مزيد من المعلومات", +"Primary address (Kontact et al)" => "العنوان الرئيسي (جهات الإتصال)", +"iOS/OS X" => "ط ن ت/ ن ت 10", +"Read only iCalendar link(s)" => "اقرأ وصلة(ت) التقويم فقط" +); diff --git a/l10n/be.php b/l10n/be.php new file mode 100644 index 000000000..35f152a77 --- /dev/null +++ b/l10n/be.php @@ -0,0 +1,3 @@ + "Дасведчаны" +); diff --git a/l10n/bg_BG.php b/l10n/bg_BG.php new file mode 100644 index 000000000..2e078de4b --- /dev/null +++ b/l10n/bg_BG.php @@ -0,0 +1,22 @@ + "Невалидна заявка", +"Calendar" => "Календар", +"Title" => "Заглавие", +"Other" => "Други", +"Personal" => "Лични", +"never" => "никога", +"Date" => "Дата", +"Settings" => "Настройки", +"Download" => "Изтегляне", +"Edit" => "Промяна", +"Delete" => "Изтриване", +"Save" => "Запис", +"Submit" => "Потвърждение", +"Cancel" => "Отказ", +"Export" => "Експорт", +"Share" => "Споделяне", +"Category" => "Категория", +"Import" => "Внасяне", +"General" => "Общи", +"URLs" => "Уеб адрес" +); diff --git a/l10n/bn_BD.php b/l10n/bn_BD.php new file mode 100644 index 000000000..993454ce5 --- /dev/null +++ b/l10n/bn_BD.php @@ -0,0 +1,190 @@ + "সমস্ত দিনপঞ্জী সম্পূর্ণভাবে ক্যাসে রাখা নেই", +"Everything seems to be completely cached" => "মনে হচ্ছে সমস্ত কিছুই সম্পূর্ণবাবে ক্যাসে করা আছে", +"No calendars found." => "কোন দিনপঞ্জী খুঁজে পাওয়া গেল না।", +"No events found." => "কোন ইভেন্ট খুঁজে পাওয়া গেল না।", +"Wrong calendar" => "ভুল দিনপঞ্জী", +"The file contained either no events or all events are already saved in your calendar." => "The file contained either no events or all events are already saved in your calendar.", +"events has been saved in the new calendar" => "নতুন দিনপঞ্জীতে ইভেন্টগুলো সংরক্ষণ করা হয়েছে ", +"Import failed" => "আমদানি ব্যর্থ", +"events has been saved in your calendar" => "আপনার দিনপঞ্জীতে ইভেন্টগুলো সংরক্ষণ করা হয়েছে", +"New Timezone:" => "নতুন সময় এলাকাঃ", +"Timezone changed" => "সময় এলাকা পরিবর্তিত হয়েছে", +"Invalid request" => "অনুরোধটি সঠিক নয়", +"Calendar" => "দিনপঞ্জী", +"Editable" => "সম্পাদযোগ্য", +"ddd" => "দিদিদি", +"ddd M/d" => "দিদিদি মা/দি", +"dddd M/d" => "দিদিদিদি মা/দি", +"MMMM yyyy" => "মামামামা বববব", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "মামামা দি[বববব]{ '—'[মামামা] দি বববব}", +"dddd, MMM d, yyyy" => "দিদিদিদি, মামামা দি, বববব", +"Sunday" => "রবিবার", +"Monday" => "সোমবার", +"Tuesday" => "মঙ্গলবার", +"Wednesday" => "বুধবার", +"Thursday" => "বৃহস্পতিবার", +"Friday" => "শুক্রবার", +"Saturday" => "শনিবার", +"Sun." => "রবি.", +"Mon." => "সোম.", +"Tue." => "মঙ্গল.", +"Wed." => "বুধ.", +"Thu." => "বৃহঃ.", +"Fri." => "শুক্র.", +"Sat." => "শনি.", +"January" => "জানুয়ারি", +"February" => "ফেব্রুয়ারি", +"March" => "মার্চ", +"April" => "এপ্রিল", +"May" => "মে", +"June" => "জুন", +"July" => "জুলাই", +"August" => "অগাষ্ট", +"September" => "সেপ্টেম্বর", +"October" => "অক্টোবর", +"November" => "নভেম্বর", +"December" => "ডিসেম্বর", +"Jan." => "জানু.", +"Feb." => "ফেব্রু.", +"Mar." => "মার্চ.", +"Apr." => "এপ্রিল.", +"May." => "মে.", +"Jun." => "জুন.", +"Jul." => "জুলাই.", +"Aug." => "অগাস্ট.", +"Sep." => "সেপ্টে.", +"Oct." => "অক্টো.", +"Nov." => "নভে.", +"Dec." => "ডিসে.", +"All day" => "সমস্ত দিন", +"New Calendar" => "নতুন দিনপঞ্জী", +"Title" => "শিরোনাম", +"From Date" => "শুরুর তারিখ", +"From Time" => "শুরুর সময়", +"To Date" => "সমাপ্তির তারিখ", +"To Time" => "সমাপ্তির সময়", +"The event ends before it starts" => "শুরু হওয়ার পূর্বেই ইভেন্ট টি শেষ হচ্ছে", +"There was a database fail" => "মনে হচ্ছে ডাটাবেজে সমস্যা আছে", +"Birthday" => "জন্মদিন", +"Business" => "ব্যবসা", +"Call" => "কল", +"Clients" => "ক্লায়েন্ট", +"Deliverer" => "বিতরণকারী", +"Holidays" => "ছুটিরদিন", +"Ideas" => "প্রস্তাবনা", +"Journey" => "ভ্রমণ", +"Jubilee" => "জয়ন্তী", +"Meeting" => "সভা", +"Other" => "অন্যান্য", +"Personal" => "ব্যক্তিগত", +"Projects" => "প্রকল্পসমূহ", +"Questions" => "প্রশ্ন", +"Work" => "কর্মস্থল", +"by" => "কর্তৃক", +"unnamed" => "অজ্ঞাতনামা", +"Does not repeat" => "পূনঃপূন সংঘটিত নয়", +"Daily" => "দৈনিক", +"Weekly" => "সাপ্তাহিক", +"Every Weekday" => "প্রতি কর্মদিবসে", +"Bi-Weekly" => "পাক্ষিক", +"Monthly" => "মমাসিক", +"Yearly" => "বাৎসরিক", +"never" => "কখনোই নয়", +"by occurrences" => "সংঘটন অনুসারে", +"by date" => "তারিখ অনুসারে", +"by monthday" => "মাসিক দিন অনুসারে", +"by weekday" => "সপ্তাহ দিন অনুসারে", +"events week of month" => "মাসের মধ্যে ইভেন্ট সপ্তাহ", +"first" => "প্রখম", +"second" => "দ্বিতীয়", +"third" => "তৃতীয়", +"fourth" => "চতুর্থ", +"fifth" => "পঞ্চম", +"last" => "সর্বশেষ", +"by events date" => "ইভেন্টের দিন অনুসারে", +"by yearday(s)" => "বাৎসরিক দিন অনুযায়ী", +"by weeknumber(s)" => "সপ্তাহসংখ্যা অনুযায়ী", +"by day and month" => "দিন এবং মাস অনুসারে", +"Date" => "তারিখ", +"Cal." => "দিনপঞ্জী", +"Week" => "সপ্তাহ", +"Month" => "মাস", +"List" => "তালিকা", +"Today" => "আজ", +"Settings" => "নিয়ামকসমূহ", +"Your calendars" => "আপনার দিনপঞ্জীসমূহ", +"CalDav Link" => "CalDav লিংক", +"Share Calendar" => "দিনপঞ্জী ভাগাভাগি করুন", +"Download" => "ডাউনলোড", +"Edit" => "সম্পাদনা", +"Delete" => "মুছে ফেল", +"New calendar" => "নতুন দিনপঞ্জী", +"Edit calendar" => "দিনপঞ্জী সম্পাদনা", +"Displayname" => "প্রদর্শিতব্য নাম", +"Active" => "সক্রিয়", +"Calendar color" => "দিনপঞ্জী রং", +"Save" => "সংরক্ষণ ", +"Submit" => "জমা দিন", +"Cancel" => "বাতিল", +"Edit an event" => "ইভেন্ট সম্পাদনা কর", +"Export" => "রপ্তানী কর", +"Eventinfo" => "ইভেন্টের তথ্য", +"Repeating" => "পূনঃসঘটিত হবে", +"Alarm" => "সতর্কবাণী", +"Attendees" => "অংশগ্রহণকারীবৃন্দ", +"Share" => "ভাগাভাগি", +"Title of the Event" => "ইভেন্টের শিরোনাম", +"Category" => "ক্যাটেগরি", +"Separate categories with commas" => "ক্যাটগরি গুলো কমা দিয়ে পৃথক করুন", +"Edit categories" => "ক্যাটেগরি সম্পাদনা", +"All Day Event" => "দিনব্যাপী ইভেন্ট", +"From" => "শুরুর সময়", +"To" => "শেষের সময়", +"Advanced options" => "সুচারু বিকল্পসমূহ", +"Location" => "াবস্থান", +"Location of the Event" => "ইভেন্টের অবস্থান", +"Description" => "বিবরণ", +"Description of the Event" => "ইভেন্টের বিবরণ", +"Repeat" => "পূনঃসংঘটন", +"Advanced" => "সুচারু", +"Select weekdays" => "সপ্তাহদিন নির্বাচন করুন", +"Select days" => "দিন নির্বাচন", +"and the events day of year." => "এবং বছরের ইভেন্ট দিবস ।", +"and the events day of month." => "এবং মাসের ইভেন্ট দিবস।", +"Select months" => "মাস নির্বাচন", +"Select weeks" => "সপ্তাহ নির্বাচন", +"and the events week of year." => "এবং বছরের ইভেন্ট সপ্তাহ।", +"Interval" => "মধ্যবিরতি", +"End" => "সমাপ্ত", +"occurrences" => "সংঘটন", +"create a new calendar" => "নতুন দিনপঞ্জী তৈরী কর", +"Import a calendar file" => "দিনপঞ্জী ফাইল আমদানি কর", +"Please choose a calendar" => "দয়া করে একটি দিনপঞ্জী নির্বাচন করুন", +"Name of new calendar" => "নতুন দিনপঞ্জীটির নাম ", +"Take an available name!" => "সুলভ কোন একটি নাম নিন !", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "এই একই নামের একটি দিনপঞ্জী বিদ্যমান। যদি আপনি কোন প্রকারে অগ্রসর হতে চান, তবে এই গিনপঞ্জীগুলো একত্রিত করা হবে।", +"Import" => "আমদানি", +"Close Dialog" => "সংলাপ বন্ধ", +"Create a new event" => "নতুন ইভেন্ট তৈরী কর", +"Unshare" => "ভাগাভাগি বাতিল", +"View an event" => "ইভেন্ট দর্শন ", +"No categories selected" => "কোন ক্যাটেগরি নির্বাচন করা হয় নি", +"of" => "এর", +"at" => "at", +"General" => "সাধারণ", +"Timezone" => "সময় এলাকা", +"Update timezone automatically" => "সময়এলাকা স্বয়ংক্রিয়ভাবে পরিবর্ধন কর", +"Time format" => "সময় ফর্ম্যাট", +"24h" => "২৪ ঘ.", +"12h" => "১২ ঘ.", +"Start week on" => "সপ্তাহ শুরু হয়", +"Cache" => "ক্যাসে", +"Clear cache for repeating events" => "পূনঃপূন সংঘটিত ইভেন্টের জন্য ক্যাসে পরিষ্কার কর", +"URLs" => "URL গুলো", +"Calendar CalDAV syncing addresses" => "দিনপঞ্জী CalDAV সমলয় ঠিকানাসমূহ", +"more info" => "আরও তথ্য", +"Primary address (Kontact et al)" => "প্রাথমিক ঠিকানা (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "শুধুমাত্র পঠনযোগ্য iCalendar লিংক" +); diff --git a/l10n/ca.php b/l10n/ca.php new file mode 100644 index 000000000..fc8c6e38e --- /dev/null +++ b/l10n/ca.php @@ -0,0 +1,215 @@ + "No tots els calendaris estan en memòria", +"Everything seems to be completely cached" => "Sembla que tot està en memòria", +"No calendars found." => "No s'han trobat calendaris.", +"No events found." => "No s'han trobat events.", +"Wrong calendar" => "Calendari erroni", +"You do not have the permissions to edit this event." => "No teniu permisos per editar aquest esdeveniment.", +"The file contained either no events or all events are already saved in your calendar." => "El fitxer no contenia esdeveniments o aquests ja estaven desats en el vostre caledari", +"events has been saved in the new calendar" => "els esdeveniments s'han desat en el calendari nou", +"Import failed" => "Ha fallat la importació", +"events has been saved in your calendar" => "els esdveniments s'han desat en el calendari", +"New Timezone:" => "Nova zona horària:", +"Timezone changed" => "La zona horària ha canviat", +"Invalid request" => "Sol·licitud no vàlida", +"Calendar" => "Calendari", +"Deletion failed" => "Eliminació fallida", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ -[ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ -[ ddd d MMMM yyyy] HH:mm}", +"user" => "usuari", +"group" => "grup", +"Editable" => "Editable", +"Shareable" => "Es pot compartir", +"Deletable" => "Es pot eliminar", +"ddd" => "ddd", +"ddd M/d" => "ddd d/M", +"dddd M/d" => "dddd d/M", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "d [MMM ][yyyy ]{'—' d MMM yyyy}", +"dddd, MMM d, yyyy" => "dddd, d MMM, yyyy", +"Sunday" => "Diumenge", +"Monday" => "Dilluns", +"Tuesday" => "Dimarts", +"Wednesday" => "Dimecres", +"Thursday" => "Dijous", +"Friday" => "Divendres", +"Saturday" => "Dissabte", +"Sun." => "Dg.", +"Mon." => "Dl.", +"Tue." => "Dm.", +"Wed." => "Dc.", +"Thu." => "Dj.", +"Fri." => "Dv.", +"Sat." => "Ds.", +"January" => "Gener", +"February" => "Febrer", +"March" => "Març", +"April" => "Abril", +"May" => "Maig", +"June" => "Juny", +"July" => "Juliol", +"August" => "Agost", +"September" => "Setembre", +"October" => "Octubre", +"November" => "Novembre", +"December" => "Desembre", +"Jan." => "Gen.", +"Feb." => "Febr.", +"Mar." => "Març", +"Apr." => "Abr.", +"May." => "Maig", +"Jun." => "Juny", +"Jul." => "Jul.", +"Aug." => "Ag.", +"Sep." => "Set.", +"Oct." => "Oct.", +"Nov." => "Nov.", +"Dec." => "Des.", +"All day" => "Tot el dia", +"New Calendar" => "Calendari nou", +"Missing or invalid fields" => "Falten camps o no són vàlids", +"Title" => "Títol", +"From Date" => "Des de la data", +"From Time" => "Des de l'hora", +"To Date" => "Fins a la data", +"To Time" => "Fins a l'hora", +"The event ends before it starts" => "L'esdeveniment acaba abans que comenci", +"There was a database fail" => "Hi ha un error de base de dades", +"Birthday" => "Aniversari", +"Business" => "Feina", +"Call" => "Trucada", +"Clients" => "Clients", +"Deliverer" => "Remitent", +"Holidays" => "Vacances", +"Ideas" => "Idees", +"Journey" => "Viatge", +"Jubilee" => "Sant", +"Meeting" => "Reunió", +"Other" => "Altres", +"Personal" => "Personal", +"Projects" => "Projectes", +"Questions" => "Preguntes", +"Work" => "Feina", +"by" => "per", +"unnamed" => "sense nom", +"You do not have the permissions to update this calendar." => "No teniu permisos per actualitzar aquest calendari.", +"You do not have the permissions to delete this calendar." => "No teniu permisos per eliminar aquest calendari.", +"You do not have the permissions to add to this calendar." => "No teniu permisos per afegir en aquest calendari.", +"You do not have the permissions to add events to this calendar." => "No teniu permisos per afegir esdeveniments en aquest calendari.", +"You do not have the permissions to delete this event." => "No teniu permisos per eliminar aquest esdeveniment.", +"Busy" => "Ocupat", +"Public" => "Públic", +"Private" => "Privat", +"Confidential" => "Confidencial", +"Does not repeat" => "No es repeteix", +"Daily" => "Diari", +"Weekly" => "Mensual", +"Every Weekday" => "Cada setmana", +"Bi-Weekly" => "Bisetmanalment", +"Monthly" => "Mensualment", +"Yearly" => "Cada any", +"never" => "mai", +"by occurrences" => "per aparicions", +"by date" => "per data", +"by monthday" => "per dia del mes", +"by weekday" => "per dia de la setmana", +"events week of month" => "esdeveniments la setmana del mes", +"first" => "primer", +"second" => "segon", +"third" => "tercer", +"fourth" => "quart", +"fifth" => "cinquè", +"last" => "últim", +"by events date" => "per data d'esdeveniments", +"by yearday(s)" => "per ahir(s)", +"by weeknumber(s)" => "per número(s) de la setmana", +"by day and month" => "per dia del mes", +"Date" => "Data", +"Cal." => "Cal.", +"Week" => "Setmana", +"Month" => "Mes", +"List" => "Llista", +"Today" => "Avui", +"Settings" => "Configuració", +"Your calendars" => "Els vostres calendaris", +"CalDav Link" => "Enllaç CalDav", +"Share Calendar" => "Comparteix el calendari", +"Download" => "Baixa", +"Edit" => "Edita", +"Delete" => "Suprimeix", +"New calendar" => "Calendari nou", +"Edit calendar" => "Edita el calendari", +"Displayname" => "Mostra el nom", +"Active" => "Actiu", +"Calendar color" => "Color del calendari", +"Save" => "Desa", +"Submit" => "Envia", +"Cancel" => "Cancel·la", +"Edit an event" => "Edició d'un esdeveniment", +"Export" => "Exporta", +"Eventinfo" => "Eventinfo", +"Repeating" => "Repetició", +"Alarm" => "Alarma", +"Attendees" => "Assistents", +"Share" => "Comparteix", +"Title of the Event" => "Títol de l'esdeveniment", +"Category" => "Categoria", +"Separate categories with commas" => "Separeu les categories amb comes", +"Edit categories" => "Edita les categories", +"Access Class" => "Tipus d'accés", +"All Day Event" => "Esdeveniment de tot el dia", +"From" => "Des de", +"To" => "Fins a", +"Advanced options" => "Opcions avançades", +"Location" => "Ubicació", +"Location of the Event" => "Ubicació de l'esdeveniment", +"Description" => "Descripció", +"Description of the Event" => "Descripció de l'esdeveniment", +"Repeat" => "Repetició", +"Advanced" => "Avançat", +"Select weekdays" => "Selecciona els dies de la setmana", +"Select days" => "Seleccionar dies", +"and the events day of year." => "i dies d'esdeveniment de l'any.", +"and the events day of month." => "i dies d'esdeveniment del mes.", +"Select months" => "Selecciona els mesos", +"Select weeks" => "Seleccionar setmanes", +"and the events week of year." => "i setmanes d'esdeveniment de l'any.", +"Interval" => "Interval", +"End" => "Final", +"occurrences" => "aparicions", +"create a new calendar" => "crea un calendari nou", +"Import a calendar file" => "Importa un fitxer de calendari", +"Please choose a calendar" => "Escolliu un calendari", +"Name of new calendar" => "Nom del calendari nou", +"Take an available name!" => "Escolliu un nom disponible!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Ja hi ha un calendari amb aquest nom. Si continueu, els calendaris es combinaran.", +"Remove all events from the selected calendar" => "Elimina tots els esdeveniments del calendari seleccionat", +"Import" => "Importa", +"Close Dialog" => "Tanca el diàleg", +"Create a new event" => "Crea un nou esdeveniment", +"Share with:" => "Comparteix amb:", +"Shared with" => "Compartit per", +"Unshare" => "Descomparteix", +"Nobody" => "Ningú", +"Shared via calendar" => "Compartit via calendari", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTA: Les accions en esdeveniments compartits via calendari afectaran la compartició del calendari sencer.", +"View an event" => "Mostra un esdeveniment", +"No categories selected" => "No hi ha categories seleccionades", +"of" => "de", +"at" => "a", +"General" => "General", +"Timezone" => "Zona horària", +"Update timezone automatically" => "Actualitza la zona horària automàticament", +"Time format" => "Format horari", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Comença la setmana en ", +"Cache" => "Memòria de cau", +"Clear cache for repeating events" => "Neteja la memòria de cau pels esdeveniments amb repetició", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Adreça de sincronització del calendari CalDAV", +"more info" => "més informació", +"Primary address (Kontact et al)" => "Adreça primària (Kontact et al)", +"iOS/OS X" => "IOS/OS X", +"Read only iCalendar link(s)" => "Enllaç(os) iCalendar només de lectura" +); diff --git a/l10n/cs_CZ.php b/l10n/cs_CZ.php new file mode 100644 index 000000000..3f44ba57d --- /dev/null +++ b/l10n/cs_CZ.php @@ -0,0 +1,215 @@ + "V paměti nejsou uloženy úplně všechny kalendáře", +"Everything seems to be completely cached" => "Zdá se, že úplně vše je uloženo v paměti", +"No calendars found." => "Žádné kalendáře nenalezeny.", +"No events found." => "Žádné události nenalezeny.", +"Wrong calendar" => "Nesprávný kalendář", +"You do not have the permissions to edit this event." => "Nemáte práva upravovat tuto událost.", +"The file contained either no events or all events are already saved in your calendar." => "Soubor neobsahoval žádné události, nebo jsou všechny události již ve Vašem kalendáři.", +"events has been saved in the new calendar" => "události byly uloženy v novém kalendáři", +"Import failed" => "Import selhal", +"events has been saved in your calendar" => "událostí bylo uloženo ve Vašem kalendáři", +"New Timezone:" => "Nové časové pásmo:", +"Timezone changed" => "Časové pásmo bylo změněno", +"Invalid request" => "Neplatný požadavek", +"Calendar" => "Kalendář", +"Deletion failed" => "Mazání selhalo.", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "uživatel", +"group" => "skupina", +"Editable" => "Upravitelné", +"Shareable" => "Sdílitelné", +"Deletable" => "Odstranitelné", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "d. MMM[ yyyy]{ '—' d.[ MMM] yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Neděle", +"Monday" => "Pondělí", +"Tuesday" => "Úterý", +"Wednesday" => "Středa", +"Thursday" => "Čtvrtek", +"Friday" => "Pátek", +"Saturday" => "Sobota", +"Sun." => "Ne", +"Mon." => "Po", +"Tue." => "Út", +"Wed." => "St", +"Thu." => "Čt", +"Fri." => "Pá", +"Sat." => "So", +"January" => "Leden", +"February" => "Únor", +"March" => "Březen", +"April" => "Duben", +"May" => "Květen", +"June" => "Červen", +"July" => "Červenec", +"August" => "Srpen", +"September" => "Září", +"October" => "Říjen", +"November" => "Listopad", +"December" => "Prosinec", +"Jan." => "leden", +"Feb." => "únor", +"Mar." => "březen", +"Apr." => "duben", +"May." => "květen", +"Jun." => "červen", +"Jul." => "červenec", +"Aug." => "srpen", +"Sep." => "září", +"Oct." => "říjen", +"Nov." => "listopad", +"Dec." => "prosinec", +"All day" => "Celý den", +"New Calendar" => "Nový kalendář", +"Missing or invalid fields" => "Chybějící, nebo neplatná pole", +"Title" => "Název", +"From Date" => "Od data", +"From Time" => "Od", +"To Date" => "Do data", +"To Time" => "Do", +"The event ends before it starts" => "Událost končí před svým zahájením", +"There was a database fail" => "Chyba v databázi", +"Birthday" => "Narozeniny", +"Business" => "Obchodní", +"Call" => "Hovor", +"Clients" => "Klienti", +"Deliverer" => "Doručovatel", +"Holidays" => "Prázdniny", +"Ideas" => "Nápady", +"Journey" => "Cesta", +"Jubilee" => "Výročí", +"Meeting" => "Schůzka", +"Other" => "Jiné", +"Personal" => "Osobní", +"Projects" => "Projekty", +"Questions" => "Dotazy", +"Work" => "Práce", +"by" => "od", +"unnamed" => "nepojmenováno", +"You do not have the permissions to update this calendar." => "Nemáte práva pro aktualizaci tohoto kalendáře.", +"You do not have the permissions to delete this calendar." => "Nemáte práva pro smazání tohoto kalendáře.", +"You do not have the permissions to add to this calendar." => "Nemáte práva pro přidání tohoto kalendáře.", +"You do not have the permissions to add events to this calendar." => "Nemáte práva pro přidání událostí do tohoto kalendáře.", +"You do not have the permissions to delete this event." => "Nemáte práva smazat tuto událost.", +"Busy" => "Zaneprázdněná", +"Public" => "Veřejná", +"Private" => "Soukromá", +"Confidential" => "Důvěrná", +"Does not repeat" => "Neopakuje se", +"Daily" => "Denně", +"Weekly" => "Týdně", +"Every Weekday" => "Každý všední den", +"Bi-Weekly" => "Jednou za dva týdny", +"Monthly" => "Měsíčně", +"Yearly" => "Ročně", +"never" => "nikdy", +"by occurrences" => "podle výskytu", +"by date" => "podle data", +"by monthday" => "podle dne v měsíci", +"by weekday" => "podle dne v týdnu", +"events week of month" => "týdenní události v měsíci", +"first" => "první", +"second" => "druhý", +"third" => "třetí", +"fourth" => "čtvrtý", +"fifth" => "pátý", +"last" => "poslední", +"by events date" => "podle data události", +"by yearday(s)" => "po dni (dnech)", +"by weeknumber(s)" => "podle čísel týdnů", +"by day and month" => "podle dne a měsíce", +"Date" => "Datum", +"Cal." => "Kal.", +"Week" => "Týden", +"Month" => "Měsíc", +"List" => "Seznam", +"Today" => "Dnes", +"Settings" => "Nastavení", +"Your calendars" => "Vaše kalendáře", +"CalDav Link" => "Odkaz CalDav", +"Share Calendar" => "Sdílet kalendář", +"Download" => "Stáhnout", +"Edit" => "Upravit", +"Delete" => "Smazat", +"New calendar" => "Nový kalendář", +"Edit calendar" => "Upravit kalendář", +"Displayname" => "Zobrazované jméno", +"Active" => "Aktivní", +"Calendar color" => "Barva kalendáře", +"Save" => "Uložit", +"Submit" => "Odeslat", +"Cancel" => "Zrušit", +"Edit an event" => "Upravit událost", +"Export" => "Exportovat", +"Eventinfo" => "Informace o události", +"Repeating" => "Opakování", +"Alarm" => "Upomínka", +"Attendees" => "Účastníci", +"Share" => "Sdílet", +"Title of the Event" => "Název události", +"Category" => "Kategorie", +"Separate categories with commas" => "Kategorie oddělené čárkami", +"Edit categories" => "Upravit kategorie", +"Access Class" => "Třída přístupu", +"All Day Event" => "Celodenní událost", +"From" => "Od", +"To" => "Do", +"Advanced options" => "Pokročilé možnosti", +"Location" => "Umístění", +"Location of the Event" => "Místo konání události", +"Description" => "Popis", +"Description of the Event" => "Popis události", +"Repeat" => "Opakovat", +"Advanced" => "Pokročilé", +"Select weekdays" => "Vybrat dny v týdnu", +"Select days" => "Vybrat dny", +"and the events day of year." => "a denní události v roce.", +"and the events day of month." => "a denní události v měsíci.", +"Select months" => "Vybrat měsíce", +"Select weeks" => "Vybrat týdny", +"and the events week of year." => "a týden s událostmi v roce.", +"Interval" => "Interval", +"End" => "Konec", +"occurrences" => "výskyty", +"create a new calendar" => "vytvořit nový kalendář", +"Import a calendar file" => "Importovat soubor kalendáře", +"Please choose a calendar" => "Vyberte, prosím, kalendář", +"Name of new calendar" => "Název nového kalendáře", +"Take an available name!" => "Použijte dostupný název.", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Kalendář s tímto názvem již existuje. Pokud název použijete, budou tyto kalendáře sloučeny.", +"Remove all events from the selected calendar" => "Odebrat všechny události z vybraného kalendáře", +"Import" => "Importovat", +"Close Dialog" => "Zavřít dialog", +"Create a new event" => "Vytvořit novou událost", +"Share with:" => "Sdílet s:", +"Shared with" => "Sdíleno s", +"Unshare" => "Od-sdílet", +"Nobody" => "Nikdo", +"Shared via calendar" => "Sdíleno skrze kalendář", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "POZNÁMKA: Činnosti na událostech sdílených skrze kalendář budou mít vliv na sdílení celého kalendáře.", +"View an event" => "Zobrazit událost", +"No categories selected" => "Žádné kategorie nevybrány", +"of" => "z", +"at" => "v", +"General" => "Hlavní", +"Timezone" => "Časové pásmo", +"Update timezone automatically" => "Aktualizovat automaticky časové pásmo", +"Time format" => "Formát času", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Týden začíná v", +"Cache" => "Vyrovnávací paměť", +"Clear cache for repeating events" => "Vymazat paměť pro opakující se události", +"URLs" => "URL", +"Calendar CalDAV syncing addresses" => "Kalendář CalDAV synchronizuje adresy", +"more info" => "podrobnosti", +"Primary address (Kontact et al)" => "Primární adresa (veřejná)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Odkaz(y) kalendáře pouze pro čtení" +); diff --git a/l10n/da.php b/l10n/da.php new file mode 100644 index 000000000..675cc0bd1 --- /dev/null +++ b/l10n/da.php @@ -0,0 +1,215 @@ + "Ikke alle kalendere er fuldstændig cached", +"Everything seems to be completely cached" => "Alt ser ud til at være cached", +"No calendars found." => "Der blev ikke fundet nogen kalendere.", +"No events found." => "Der blev ikke fundet nogen begivenheder.", +"Wrong calendar" => "Forkert kalender", +"You do not have the permissions to edit this event." => "Du har ikke rettigheder til at redigere denne begivenhed.", +"The file contained either no events or all events are already saved in your calendar." => "Enten indeholdt filen ingen begivenheder, eller også er alle begivenheder allerede gemt i din kalender.", +"events has been saved in the new calendar" => "begivenheder er gemt i den nye kalender", +"Import failed" => "Import mislykkedes", +"events has been saved in your calendar" => "begivenheder er gemt i din kalender", +"New Timezone:" => "Ny tidszone:", +"Timezone changed" => "Tidszone ændret", +"Invalid request" => "Ugyldig forespørgsel", +"Calendar" => "Kalender", +"Deletion failed" => "Fejl ved sletning", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "bruger", +"group" => "gruppe", +"Editable" => "Redigerbar", +"Shareable" => "Kan deles", +"Deletable" => "Kan slettes", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Søndag", +"Monday" => "Mandag", +"Tuesday" => "Tirsdag", +"Wednesday" => "Onsdag", +"Thursday" => "Torsdag", +"Friday" => "Fredag", +"Saturday" => "Lørdag", +"Sun." => "Søn.", +"Mon." => "Man.", +"Tue." => "Tir.", +"Wed." => "Ons.", +"Thu." => "Tor.", +"Fri." => "Fre.", +"Sat." => "Lør.", +"January" => "Januar", +"February" => "Februar", +"March" => "Marts", +"April" => "April", +"May" => "Maj", +"June" => "Juni", +"July" => "Juli", +"August" => "August", +"September" => "September", +"October" => "Oktober", +"November" => "November", +"December" => "December", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Apr.", +"May." => "Maj", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dec.", +"All day" => "Hele dagen", +"New Calendar" => "Ny kalender", +"Missing or invalid fields" => "Manglende eller ugyldige felter", +"Title" => "Titel", +"From Date" => "Fra dato", +"From Time" => "Fra tidspunkt", +"To Date" => "Til dato", +"To Time" => "Til tidspunkt", +"The event ends before it starts" => "Begivenheden slutter, inden den begynder", +"There was a database fail" => "Der var en fejl i databasen", +"Birthday" => "Fødselsdag", +"Business" => "Erhverv", +"Call" => "Ring", +"Clients" => "Kunder", +"Deliverer" => "Leverance", +"Holidays" => "Helligdage", +"Ideas" => "Ideer", +"Journey" => "Rejse", +"Jubilee" => "Jubilæum", +"Meeting" => "Møde", +"Other" => "Andet", +"Personal" => "Privat", +"Projects" => "Projekter", +"Questions" => "Spørgsmål", +"Work" => "Arbejde", +"by" => "af", +"unnamed" => "unavngivet", +"You do not have the permissions to update this calendar." => "Du har ikke rettigheder til at opdatere denne kalender.", +"You do not have the permissions to delete this calendar." => "Du har ikke rettigheder til at slette denne kalender.", +"You do not have the permissions to add to this calendar." => "Du har ikke rettigheder til at tilføje til denne kalender.", +"You do not have the permissions to add events to this calendar." => "Du har ikke rettigheder til at tilføje begivenheder til denne kalender.", +"You do not have the permissions to delete this event." => "Du har ikke rettigheder til at slette denne begivenhed.", +"Busy" => "Optaget", +"Public" => "Offentlig", +"Private" => "Privat", +"Confidential" => "Fortrolig", +"Does not repeat" => "Gentages ikke", +"Daily" => "Dagligt", +"Weekly" => "Ugentligt", +"Every Weekday" => "Alle hverdage", +"Bi-Weekly" => "Hver anden uge", +"Monthly" => "Månedligt", +"Yearly" => "Årligt", +"never" => "aldrig", +"by occurrences" => "efter forekomster", +"by date" => "efter dato", +"by monthday" => "efter dag i måneden", +"by weekday" => "efter ugedag", +"events week of month" => "begivenhedens uge i måneden", +"first" => "første", +"second" => "anden", +"third" => "tredje", +"fourth" => "fjerde", +"fifth" => "femte", +"last" => "sidste", +"by events date" => "efter begivenheders dato", +"by yearday(s)" => "efter dag(e) i året", +"by weeknumber(s)" => "efter ugenummer/-numre", +"by day and month" => "efter dag og måned", +"Date" => "Dato", +"Cal." => "Kal.", +"Week" => "Uge", +"Month" => "Måned", +"List" => "Liste", +"Today" => "I dag", +"Settings" => "Indstillinger", +"Your calendars" => "Dine kalendere", +"CalDav Link" => "CalDav-link", +"Share Calendar" => "Del kalender", +"Download" => "Hent", +"Edit" => "Rediger", +"Delete" => "Slet", +"New calendar" => "Ny kalender", +"Edit calendar" => "Rediger kalender", +"Displayname" => "Vist navn", +"Active" => "Aktiv", +"Calendar color" => "Kalenderfarve", +"Save" => "Gem", +"Submit" => "Send", +"Cancel" => "Annuller", +"Edit an event" => "Rediger en begivenhed", +"Export" => "Eksporter", +"Eventinfo" => "Begivenhedsinfo", +"Repeating" => "Gentagende", +"Alarm" => "Alarm", +"Attendees" => "Deltagere", +"Share" => "Del", +"Title of the Event" => "Titel på begivenheden", +"Category" => "Kategori", +"Separate categories with commas" => "Adskil kategorier med kommaer", +"Edit categories" => "Rediger kategorier", +"Access Class" => "Adgangsklasse", +"All Day Event" => "Heldagsarrangement", +"From" => "Fra", +"To" => "Til", +"Advanced options" => "Avancerede indstillinger", +"Location" => "Sted", +"Location of the Event" => "Placering af begivenheden", +"Description" => "Beskrivelse", +"Description of the Event" => "Beskrivelse af begivenheden", +"Repeat" => "Gentag", +"Advanced" => "Avanceret", +"Select weekdays" => "Vælg ugedage", +"Select days" => "Vælg dage", +"and the events day of year." => "og begivenhedens dag i året.", +"and the events day of month." => "og begivenhedens dag i måneden", +"Select months" => "Vælg måneder", +"Select weeks" => "Vælg uger", +"and the events week of year." => "og begivenhedens uge i året.", +"Interval" => "Interval", +"End" => "Afslutning", +"occurrences" => "forekomster", +"create a new calendar" => "opret en ny kalender", +"Import a calendar file" => "Importer en kalenderfil", +"Please choose a calendar" => "Vælg en kalender", +"Name of new calendar" => "Navn på ny kalender", +"Take an available name!" => "Vælg et ledigt navn!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "En kalender med dette navn findes allerede. Hvis du fortsætter alligevel, vil disse kalendere blive sammenlagt.", +"Remove all events from the selected calendar" => "Fjern alle events fra den valgte kalender", +"Import" => "Importer", +"Close Dialog" => "Luk dialog", +"Create a new event" => "Opret en ny begivenhed", +"Share with:" => "Del med:", +"Shared with" => "Delt med", +"Unshare" => "Fjern deling", +"Nobody" => "Ingen", +"Shared via calendar" => "Delt via kalender", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NB! Handlinger på begivenheder delt via kalenderen vil berøre hele kalenderdelingen.", +"View an event" => "Vis en begivenhed", +"No categories selected" => "Ingen categorier valgt", +"of" => "fra", +"at" => "kl.", +"General" => "Generel", +"Timezone" => "Tidszone", +"Update timezone automatically" => "Opdater tidszone automatisk", +"Time format" => "Tidsformat", +"24h" => "24T", +"12h" => "12T", +"Start week on" => "Start ugen med", +"Cache" => "Cache", +"Clear cache for repeating events" => "Ryd cache for gentagende begivenheder", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Adresser til kalendersynkronisering over CalDAV", +"more info" => "flere oplysninger", +"Primary address (Kontact et al)" => "Primær adresse (Kontakt o.a.)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Skrivebeskyttet iCalendar-link(s)" +); diff --git a/l10n/de.php b/l10n/de.php new file mode 100644 index 000000000..4af73048b --- /dev/null +++ b/l10n/de.php @@ -0,0 +1,215 @@ + "Noch sind nicht alle Kalender zwischengespeichert.", +"Everything seems to be completely cached" => "Es sieht so aus, als wäre alles vollständig zwischengespeichert.", +"No calendars found." => "Keine Kalender gefunden.", +"No events found." => "Keine Termine gefunden.", +"Wrong calendar" => "Falscher Kalender", +"You do not have the permissions to edit this event." => "Du besitzt nicht die Berechtigung, diese Veranstaltung zu bearbeiten.", +"The file contained either no events or all events are already saved in your calendar." => "Entweder enthielt die Datei keine Termine oder alle Termine waren bereits im Kalender gespeichert.", +"events has been saved in the new calendar" => "Der Termin wurde im neuen Kalender gespeichert.", +"Import failed" => "Import fehlgeschlagen", +"events has been saved in your calendar" => "Der Termin wurde in Deinem Kalender gespeichert.", +"New Timezone:" => "Neue Zeitzone:", +"Timezone changed" => "Zeitzone geändert", +"Invalid request" => "Fehlerhafte Anfrage", +"Calendar" => "Kalender", +"Deletion failed" => "Löschen fehlgeschlagen", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "Benutzer", +"group" => "Gruppe", +"Editable" => "editierbar", +"Shareable" => "Kann geteilt werden", +"Deletable" => "Kann gelöscht werden", +"ddd" => "ddd", +"ddd M/d" => "ddd d.M", +"dddd M/d" => "dddd d.M", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, d. MMM yyyy", +"Sunday" => "Sonntag", +"Monday" => "Montag", +"Tuesday" => "Dienstag", +"Wednesday" => "Mittwoch", +"Thursday" => "Donnerstag", +"Friday" => "Freitag", +"Saturday" => "Samstag", +"Sun." => "So", +"Mon." => "Mo", +"Tue." => "Di", +"Wed." => "Mi", +"Thu." => "Do", +"Fri." => "Fr", +"Sat." => "Sa", +"January" => "Januar", +"February" => "Februar", +"March" => "März", +"April" => "April", +"May" => "Mai", +"June" => "Juni", +"July" => "Juli", +"August" => "August", +"September" => "September", +"October" => "Oktober", +"November" => "November", +"December" => "Dezember", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mär.", +"Apr." => "Apr.", +"May." => "Mai", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dez.", +"All day" => "Ganztägig", +"New Calendar" => "Neuer Kalender", +"Missing or invalid fields" => "Fehlende oder ungültige Felder", +"Title" => "Titel", +"From Date" => "Startdatum", +"From Time" => "Startzeit", +"To Date" => "Enddatum", +"To Time" => "Endzeit", +"The event ends before it starts" => "Der Termin endet, bevor er angefangen hat.", +"There was a database fail" => "Es ist ein Datenbankfehler aufgetreten", +"Birthday" => "Geburtstag", +"Business" => "Geschäftlich", +"Call" => "Anruf", +"Clients" => "Kunden", +"Deliverer" => "Lieferant", +"Holidays" => "Urlaub", +"Ideas" => "Ideen", +"Journey" => "Reise", +"Jubilee" => "Jubiläum", +"Meeting" => "Treffen", +"Other" => "Anderes", +"Personal" => "Persönlich", +"Projects" => "Projekte", +"Questions" => "Fragen", +"Work" => "Arbeit", +"by" => "von", +"unnamed" => "unbenannt", +"You do not have the permissions to update this calendar." => "Du besitzt nicht die Berechtigung, diesen Kalender zu aktualisieren.", +"You do not have the permissions to delete this calendar." => "Du besitzt nicht die Berechtigung, diesen Kalender zu löschen.", +"You do not have the permissions to add to this calendar." => "Du besitzt nicht die Berechtigung, diesem Kalender etwas hinzuzufügen.", +"You do not have the permissions to add events to this calendar." => "Du besitzt nicht die Berechtigung, diesem Kalender eine Veranstaltung hinzuzufügen.", +"You do not have the permissions to delete this event." => "Du besitzt nicht die Berechtigung, diese Veranstaltung zu löschen.", +"Busy" => "Beschäftigt", +"Public" => "Öffentlich", +"Private" => "Privat", +"Confidential" => "Vertraulich", +"Does not repeat" => "einmalig", +"Daily" => "täglich", +"Weekly" => "wöchentlich", +"Every Weekday" => "jeden Wochentag", +"Bi-Weekly" => "jede zweite Woche", +"Monthly" => "monatlich", +"Yearly" => "jährlich", +"never" => "niemals", +"by occurrences" => "nach Terminen", +"by date" => "nach Datum", +"by monthday" => "an einem Monatstag", +"by weekday" => "an einem Wochentag", +"events week of month" => "Woche des Monats des Termins", +"first" => "erste", +"second" => "zweite", +"third" => "dritte", +"fourth" => "vierte", +"fifth" => "fünfte", +"last" => "letzte", +"by events date" => "nach Datum des Termins", +"by yearday(s)" => "nach Tag des Jahres", +"by weeknumber(s)" => "nach Wochennummer", +"by day and month" => "nach Tag und Monat", +"Date" => "Datum", +"Cal." => "Kal.", +"Week" => "Woche", +"Month" => "Monat", +"List" => "Liste", +"Today" => "Heute", +"Settings" => "Einstellungen", +"Your calendars" => "Deine Kalender", +"CalDav Link" => "CalDAV-Link", +"Share Calendar" => "Kalender freigeben", +"Download" => "Herunterladen", +"Edit" => "Bearbeiten", +"Delete" => "Löschen", +"New calendar" => "Neuer Kalender", +"Edit calendar" => "Kalender bearbeiten", +"Displayname" => "Anzeigename", +"Active" => "Aktiv", +"Calendar color" => "Kalenderfarbe", +"Save" => "Speichern", +"Submit" => "Bestätigen", +"Cancel" => "Abbrechen", +"Edit an event" => "Ereignis bearbeiten", +"Export" => "Exportieren", +"Eventinfo" => "Termininfo", +"Repeating" => "Wiederholen", +"Alarm" => "Alarm", +"Attendees" => "Teilnehmer", +"Share" => "Freigeben", +"Title of the Event" => "Titel", +"Category" => "Kategorie", +"Separate categories with commas" => "Kategorien mit Kommata trennen", +"Edit categories" => "Kategorien ändern", +"Access Class" => "Zugriffsklasse", +"All Day Event" => "Ganztägiges Ereignis", +"From" => "von", +"To" => "bis", +"Advanced options" => "Erweiterte Optionen", +"Location" => "Ort", +"Location of the Event" => "Ort", +"Description" => "Beschreibung", +"Description of the Event" => "Beschreibung", +"Repeat" => "Wiederholen", +"Advanced" => "Erweitert", +"Select weekdays" => "Wochentage auswählen", +"Select days" => "Tage auswählen", +"and the events day of year." => "und den Tag des Jahres des Termins", +"and the events day of month." => "und den Tag des Monats des Termins", +"Select months" => "Monate auswählen", +"Select weeks" => "Wochen auswählen", +"and the events week of year." => "und den Tag des Jahres des Termins", +"Interval" => "Intervall", +"End" => "Ende", +"occurrences" => "Ereignisse", +"create a new calendar" => "Neuen Kalender anlegen", +"Import a calendar file" => "Kalenderdatei importieren", +"Please choose a calendar" => "Bitte wähle einen Kalender.", +"Name of new calendar" => "Name des neuen Kalenders", +"Take an available name!" => "Bitte wähle einen verfügbaren Namen!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Ein Kalender mit diesem Namen existiert bereits. Solltest Du fortfahren, werden die beiden Kalender zusammengeführt.", +"Remove all events from the selected calendar" => "Entferne alle Termine von dem ausgewählten Kalender", +"Import" => "Importieren", +"Close Dialog" => "Dialog schließen", +"Create a new event" => "Neues Ereignis erstellen", +"Share with:" => "Teile mit:", +"Shared with" => "Geteilt mit", +"Unshare" => "Teilung zurücknehmen", +"Nobody" => "Niemand", +"Shared via calendar" => "Mittels des Kalenders teilen", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "ANMERKUNG: Aktionen, die auf Ereignissen beruhen und mittels des Kalenders geteilt werden, werden sich auf das gesamte Kalender-Sharing auswirken.", +"View an event" => "Termin ansehen", +"No categories selected" => "Keine Kategorie ausgewählt", +"of" => "von", +"at" => "um", +"General" => "Allgemein", +"Timezone" => "Zeitzone", +"Update timezone automatically" => "Zeitzone automatisch aktualisieren", +"Time format" => "Zeitformat", +"24h" => "24 Stunden", +"12h" => "12 Stunden", +"Start week on" => "Erster Wochentag", +"Cache" => "Zwischenspeicher", +"Clear cache for repeating events" => "Lösche den Zwischenspeicher für wiederholende Veranstaltungen", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "CalDAV-Kalender gleicht Adressen ab", +"more info" => "weitere Informationen", +"Primary address (Kontact et al)" => "Primäre Adresse (Kontakt u.a.)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Nur lesende(r) iCalender-Link(s)" +); diff --git a/l10n/de_DE.php b/l10n/de_DE.php new file mode 100644 index 000000000..ff4231ada --- /dev/null +++ b/l10n/de_DE.php @@ -0,0 +1,215 @@ + "Noch sind nicht alle Kalender zwischengespeichert.", +"Everything seems to be completely cached" => "Es sieht so aus, als wäre alles vollständig zwischengespeichert.", +"No calendars found." => "Keine Kalender gefunden.", +"No events found." => "Keine Termine gefunden.", +"Wrong calendar" => "Falscher Kalender", +"You do not have the permissions to edit this event." => "Sie besitzen nicht die Berechtigung, diese Veranstaltung zu bearbeiten.", +"The file contained either no events or all events are already saved in your calendar." => "Entweder enthielt die Datei keine Termine oder alle Termine waren bereits im Kalender gespeichert.", +"events has been saved in the new calendar" => "Der Termin wurde im neuen Kalender gespeichert.", +"Import failed" => "Import fehlgeschlagen", +"events has been saved in your calendar" => "Der Termin wurde in Ihrem Kalender gespeichert.", +"New Timezone:" => "Neue Zeitzone:", +"Timezone changed" => "Zeitzone geändert", +"Invalid request" => "Fehlerhafte Anfrage", +"Calendar" => "Kalender", +"Deletion failed" => "Löschen fehlgeschlagen", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "Benutzer", +"group" => "Gruppe", +"Editable" => "editierbar", +"Shareable" => "Kann freigegeben werden", +"Deletable" => "Kann gelöscht werden", +"ddd" => "ddd", +"ddd M/d" => "ddd d.M", +"dddd M/d" => "dddd d.M", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, d. MMM yyyy", +"Sunday" => "Sonntag", +"Monday" => "Montag", +"Tuesday" => "Dienstag", +"Wednesday" => "Mittwoch", +"Thursday" => "Donnerstag", +"Friday" => "Freitag", +"Saturday" => "Samstag", +"Sun." => "So", +"Mon." => "Mo", +"Tue." => "Di", +"Wed." => "Mi", +"Thu." => "Do", +"Fri." => "Fr", +"Sat." => "Sa", +"January" => "Januar", +"February" => "Februar", +"March" => "März", +"April" => "April", +"May" => "Mai", +"June" => "Juni", +"July" => "Juli", +"August" => "August", +"September" => "September", +"October" => "Oktober", +"November" => "November", +"December" => "Dezember", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mär.", +"Apr." => "Apr.", +"May." => "Mai", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dez.", +"All day" => "Ganztägig", +"New Calendar" => "Neuer Kalender", +"Missing or invalid fields" => "Fehlende oder ungültige Felder", +"Title" => "Titel", +"From Date" => "Startdatum", +"From Time" => "Startzeit", +"To Date" => "Enddatum", +"To Time" => "Endzeit", +"The event ends before it starts" => "Der Termin endet, bevor er angefangen hat.", +"There was a database fail" => "Es ist ein Datenbankfehler aufgetreten", +"Birthday" => "Geburtstag", +"Business" => "Geschäftlich", +"Call" => "Anruf", +"Clients" => "Kunden", +"Deliverer" => "Lieferant", +"Holidays" => "Urlaub", +"Ideas" => "Ideen", +"Journey" => "Reise", +"Jubilee" => "Jubiläum", +"Meeting" => "Treffen", +"Other" => "Anderes", +"Personal" => "Persönlich", +"Projects" => "Projekte", +"Questions" => "Fragen", +"Work" => "Arbeit", +"by" => "von", +"unnamed" => "unbenannt", +"You do not have the permissions to update this calendar." => "Sie besitzen nicht die Berechtigung, diesen Kalender zu aktualisieren.", +"You do not have the permissions to delete this calendar." => "Sie besitzen nicht die Berechtigung, diesen Kalender zu löschen.", +"You do not have the permissions to add to this calendar." => "Sie besitzen nicht die Berechtigung, diesem Kalender etwas hinzuzufügen.", +"You do not have the permissions to add events to this calendar." => "Sie besitzen nicht die Berechtigung, diesem Kalender eine Veranstaltung hinzuzufügen.", +"You do not have the permissions to delete this event." => "Sie besitzen nicht die Berechtigung, diese Veranstaltung zu löschen.", +"Busy" => "Nicht verfügbar", +"Public" => "Öffentlich", +"Private" => "Privat", +"Confidential" => "Vertraulich", +"Does not repeat" => "einmalig", +"Daily" => "täglich", +"Weekly" => "wöchentlich", +"Every Weekday" => "jeden Wochentag", +"Bi-Weekly" => "jede zweite Woche", +"Monthly" => "monatlich", +"Yearly" => "jährlich", +"never" => "niemals", +"by occurrences" => "nach Terminen", +"by date" => "nach Datum", +"by monthday" => "an einem Monatstag", +"by weekday" => "an einem Wochentag", +"events week of month" => "Woche des Monats des Termins", +"first" => "erste", +"second" => "zweite", +"third" => "dritte", +"fourth" => "vierte", +"fifth" => "fünfte", +"last" => "letzte", +"by events date" => "nach Datum des Termins", +"by yearday(s)" => "nach Tag des Jahres", +"by weeknumber(s)" => "nach Wochennummer", +"by day and month" => "nach Tag und Monat", +"Date" => "Datum", +"Cal." => "Kal.", +"Week" => "Woche", +"Month" => "Monat", +"List" => "Liste", +"Today" => "Heute", +"Settings" => "Einstellungen", +"Your calendars" => "Ihre Kalender", +"CalDav Link" => "CalDAV-Link", +"Share Calendar" => "Kalender freigeben", +"Download" => "Herunterladen", +"Edit" => "Bearbeiten", +"Delete" => "Löschen", +"New calendar" => "Neuer Kalender", +"Edit calendar" => "Kalender bearbeiten", +"Displayname" => "Anzeigename", +"Active" => "Aktiv", +"Calendar color" => "Kalenderfarbe", +"Save" => "Speichern", +"Submit" => "Bestätigen", +"Cancel" => "Abbrechen", +"Edit an event" => "Ereignis bearbeiten", +"Export" => "Exportieren", +"Eventinfo" => "Termininfo", +"Repeating" => "Wiederholen", +"Alarm" => "Alarm", +"Attendees" => "Teilnehmer", +"Share" => "Freigeben", +"Title of the Event" => "Titel", +"Category" => "Kategorie", +"Separate categories with commas" => "Kategorien mit Kommata trennen", +"Edit categories" => "Kategorien ändern", +"Access Class" => "Zugriffsklasse", +"All Day Event" => "Ganztägiges Ereignis", +"From" => "von", +"To" => "bis", +"Advanced options" => "Erweiterte Optionen", +"Location" => "Ort", +"Location of the Event" => "Ort", +"Description" => "Beschreibung", +"Description of the Event" => "Beschreibung", +"Repeat" => "Wiederholen", +"Advanced" => "Erweitert", +"Select weekdays" => "Wochentage auswählen", +"Select days" => "Tage auswählen", +"and the events day of year." => "und den Tag des Jahres des Termins", +"and the events day of month." => "und den Tag des Monats des Termins", +"Select months" => "Monate auswählen", +"Select weeks" => "Wochen auswählen", +"and the events week of year." => "und den Tag des Jahres des Termins", +"Interval" => "Intervall", +"End" => "Ende", +"occurrences" => "Ereignisse", +"create a new calendar" => "Neuen Kalender anlegen", +"Import a calendar file" => "Kalenderdatei importieren", +"Please choose a calendar" => "Bitte wählen Sie einen Kalender.", +"Name of new calendar" => "Name des neuen Kalenders", +"Take an available name!" => "Bitte wählen Sie einen verfügbaren Namen!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Ein Kalender mit diesem Namen existiert bereits. Sollten Sie fortfahren, werden die beiden Kalender zusammengeführt.", +"Remove all events from the selected calendar" => "Alle Ereignisse aus dem gewählten Kalender entfernen", +"Import" => "Importieren", +"Close Dialog" => "Dialog schließen", +"Create a new event" => "Neues Ereignis erstellen", +"Share with:" => "Teilen mit:", +"Shared with" => "Geteilt mit", +"Unshare" => "Teilung aufheben", +"Nobody" => "Niemand", +"Shared via calendar" => "Mittels des Kalenders geteilt", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "ANMERKUNG: Aktionen, die auf Ereignissen beruhen und mittels des Kalenders geteilt werden, werden sich auf das gesamte Kalender-Sharing auswirken.", +"View an event" => "Termin ansehen", +"No categories selected" => "Keine Kategorie ausgewählt", +"of" => "von", +"at" => "um", +"General" => "Allgemein", +"Timezone" => "Zeitzone", +"Update timezone automatically" => "Zeitzone automatisch aktualisieren", +"Time format" => "Zeitformat", +"24h" => "24 Stunden", +"12h" => "12 Stunden", +"Start week on" => "Erster Wochentag", +"Cache" => "Zwischenspeicher", +"Clear cache for repeating events" => "Lösche den Zwischenspeicher für wiederholende Veranstaltungen", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "CalDAV-Kalender gleicht Adressen ab", +"more info" => "weitere Informationen", +"Primary address (Kontact et al)" => "Primäre Adresse (Kontakt u.a.)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Nur lesende(r) iCalender-Link(s)" +); diff --git a/l10n/el.php b/l10n/el.php new file mode 100644 index 000000000..c23bfc3c3 --- /dev/null +++ b/l10n/el.php @@ -0,0 +1,215 @@ + "Δεν έχει δημιουργηθεί λανθάνουσα μνήμη για όλα τα ημερολόγια", +"Everything seems to be completely cached" => "Όλα έχουν αποθηκευτεί στη cache", +"No calendars found." => "Δε βρέθηκαν ημερολόγια.", +"No events found." => "Δε βρέθηκαν συμβάντα.", +"Wrong calendar" => "Λάθος ημερολόγιο", +"You do not have the permissions to edit this event." => "Δεν έχετε δικαιώματα να επεξεργαστείτε αυτό το συμβάν.", +"The file contained either no events or all events are already saved in your calendar." => "Το αρχείο που περιέχει είτε κανένα συμβάν είτε όλα τα συμβάντα έχουν ήδη αποθηκευτεί στο ημερολόγιό σας.", +"events has been saved in the new calendar" => "τα συμβάντα αποθηκεύτηκαν σε ένα νέο ημερολόγιο", +"Import failed" => "Η εισαγωγή απέτυχε", +"events has been saved in your calendar" => "το συμβάν αποθηκεύτηκε στο ημερολογιό σου", +"New Timezone:" => "Νέα ζώνη ώρας:", +"Timezone changed" => "Η ζώνη ώρας άλλαξε", +"Invalid request" => "Μη έγκυρο αίτημα", +"Calendar" => "Ημερολόγιο", +"Deletion failed" => "Η διαγραφή απέτυχε", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "χρήστης", +"group" => "ομάδα", +"Editable" => "Επεξεργάσιμο", +"Shareable" => "Διαμοιράσιμο", +"Deletable" => "Διαγράψιμο", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Κυριακή", +"Monday" => "Δευτέρα", +"Tuesday" => "Τρίτη", +"Wednesday" => "Τετάρτη", +"Thursday" => "Πέμπτη", +"Friday" => "Παρασκευή", +"Saturday" => "Σάββατο", +"Sun." => "Κυρ.", +"Mon." => "Δευ.", +"Tue." => "Τρί.", +"Wed." => "Τετ.", +"Thu." => "Πέμ.", +"Fri." => "Παρ.", +"Sat." => "Σάβ.", +"January" => "Ιανουάριος", +"February" => "Φεβρουάριος", +"March" => "Μάρτιος", +"April" => "Απρίλιος", +"May" => "Μάϊος", +"June" => "Ιούνιος", +"July" => "Ιούλιος", +"August" => "Αύγουστος", +"September" => "Σεπτέμβριος", +"October" => "Οκτώβριος", +"November" => "Νοέμβριος", +"December" => "Δεκέμβριος", +"Jan." => "Ιαν.", +"Feb." => "Φεβ.", +"Mar." => "Μάρ.", +"Apr." => "Απρ.", +"May." => "Μαΐ.", +"Jun." => "Ιούν.", +"Jul." => "Ιούλ.", +"Aug." => "Αύγ.", +"Sep." => "Σεπ.", +"Oct." => "Οκτ.", +"Nov." => "Νοέ.", +"Dec." => "Δεκ.", +"All day" => "Ολοήμερο", +"New Calendar" => "Νέα Ημερολόγιο", +"Missing or invalid fields" => "Κενά ή μη έγκυρα πεδία", +"Title" => "Τίτλος", +"From Date" => "Από Ημερομηνία", +"From Time" => "Από Ώρα", +"To Date" => "Έως Ημερομηνία", +"To Time" => "Έως Ώρα", +"The event ends before it starts" => "Το συμβάν ολοκληρώνεται πριν από την έναρξή του", +"There was a database fail" => "Υπήρξε σφάλμα στη βάση δεδομένων", +"Birthday" => "Γενέθλια", +"Business" => "Επιχείρηση", +"Call" => "Κλήση", +"Clients" => "Πελάτες", +"Deliverer" => "Προμηθευτής", +"Holidays" => "Διακοπές", +"Ideas" => "Ιδέες", +"Journey" => "Ταξίδι", +"Jubilee" => "Γιορτή", +"Meeting" => "Συνάντηση", +"Other" => "Άλλο", +"Personal" => "Προσωπικό", +"Projects" => "Έργα", +"Questions" => "Ερωτήσεις", +"Work" => "Εργασία", +"by" => "από", +"unnamed" => "ανώνυμο", +"You do not have the permissions to update this calendar." => "Δεν έχετε δικαιώματα για να ενημερώσετε αυτό το ημερολόγιο.", +"You do not have the permissions to delete this calendar." => "Δεν έχετε δικαιώματα να διαγράψετε αυτό το ημερολόγιο.", +"You do not have the permissions to add to this calendar." => "Δεν έχετε δικαιώματα να προσθέσετε σε αυτό το ημερολόγιο.", +"You do not have the permissions to add events to this calendar." => "Δεν έχετε δικαιώματα να προσθέστε συμβάντα σε αυτό το ημερολόγιο.", +"You do not have the permissions to delete this event." => "Δεν έχετε δικαιώματα να διαγράψετε αυτό το συμβάν.", +"Busy" => "Απασχολημένος", +"Public" => "Δημόσιο", +"Private" => "Ιδιωτικό", +"Confidential" => "Εμπιστευτικό", +"Does not repeat" => "Μη επαναλαμβανόμενο", +"Daily" => "Καθημερινά", +"Weekly" => "Εβδομαδιαία", +"Every Weekday" => "Κάθε μέρα", +"Bi-Weekly" => "Δύο φορές την εβδομάδα", +"Monthly" => "Μηνιαία", +"Yearly" => "Ετήσια", +"never" => "ποτέ", +"by occurrences" => "κατά συχνότητα πρόσβασης", +"by date" => "κατά ημερομηνία", +"by monthday" => "κατά ημέρα", +"by weekday" => "κατά εβδομάδα", +"events week of month" => "συμβάντα της εβδομάδας του μήνα", +"first" => "πρώτο", +"second" => "δεύτερο", +"third" => "τρίτο", +"fourth" => "τέταρτο", +"fifth" => "πέμπτο", +"last" => "τελευταίο", +"by events date" => "κατά ημερομηνία συμβάντων", +"by yearday(s)" => "κατά ημέρα(ες) του έτους", +"by weeknumber(s)" => "κατά εβδομάδα(ες)", +"by day and month" => "κατά ημέρα και μήνα", +"Date" => "Ημερομηνία", +"Cal." => "Ημερ.", +"Week" => "Εβδομάδα", +"Month" => "Μήνας", +"List" => "Λίστα", +"Today" => "Σήμερα", +"Settings" => "Ρυθμίσεις", +"Your calendars" => "Τα ημερολόγια σου", +"CalDav Link" => "Σύνδεση CalDAV", +"Share Calendar" => "Διαμοίρασε ένα ημερολόγιο", +"Download" => "Λήψη", +"Edit" => "Επεξεργασία", +"Delete" => "Διαγραφή", +"New calendar" => "Νέο ημερολόγιο", +"Edit calendar" => "Επεξεργασία ημερολογίου", +"Displayname" => "Προβολή ονόματος", +"Active" => "Ενεργό", +"Calendar color" => "Χρώμα ημερολογίου", +"Save" => "Αποθήκευση", +"Submit" => "Υποβολή", +"Cancel" => "Ακύρωση", +"Edit an event" => "Επεξεργασία ενός συμβάντος", +"Export" => "Εξαγωγή", +"Eventinfo" => "Πληροφορίες συμβάντος", +"Repeating" => "Επαναλαμβανόμενο", +"Alarm" => "Ειδοποίηση", +"Attendees" => "Συμμετέχοντες", +"Share" => "Διαμοίρασε", +"Title of the Event" => "Τίτλος συμβάντος", +"Category" => "Κατηγορία", +"Separate categories with commas" => "Διαχώρισε τις κατηγορίες με κόμμα", +"Edit categories" => "Επεξεργασία κατηγοριών", +"Access Class" => "Κατηγορία Πρόσβασης", +"All Day Event" => "Ολοήμερο συμβάν", +"From" => "Από", +"To" => "Έως", +"Advanced options" => "Επιλογές για προχωρημένους", +"Location" => "Τοποθεσία", +"Location of the Event" => "Τοποθεσία συμβάντος", +"Description" => "Περιγραφή", +"Description of the Event" => "Περιγραφή του συμβάντος", +"Repeat" => "Επαναλαμβανόμενο", +"Advanced" => "Για προχωρημένους", +"Select weekdays" => "Επιλογή ημερών εβδομάδας", +"Select days" => "Επιλογή ημερών", +"and the events day of year." => "και των ημερών του χρόνου που υπάρχουν συμβάντα.", +"and the events day of month." => "και των ημερών του μήνα που υπάρχουν συμβάντα.", +"Select months" => "Επιλογή μηνών", +"Select weeks" => "Επιλογή εβδομάδων", +"and the events week of year." => "και των εβδομάδων του χρόνου που υπάρουν συμβάντα.", +"Interval" => "Διάστημα", +"End" => "Τέλος", +"occurrences" => "περιστατικά", +"create a new calendar" => "δημιουργία νέου ημερολογίου", +"Import a calendar file" => "Εισαγωγή αρχείου ημερολογίου", +"Please choose a calendar" => "Παρακαλώ επέλεξε ένα ημερολόγιο", +"Name of new calendar" => "Όνομα νέου ημερολογίου", +"Take an available name!" => "Επέλεξε ένα διαθέσιμο όνομα!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Ένα ημερολόγιο με αυτό το όνομα υπάρχει ήδη. Εάν θέλετε να συνεχίσετε, αυτά τα 2 ημερολόγια θα συγχωνευθούν.", +"Remove all events from the selected calendar" => "Αφαίρεση όλων των γεγονότων από το επιλεγμένο ημερολόγιο", +"Import" => "Εισαγωγή", +"Close Dialog" => "Κλείσιμο Διαλόγου", +"Create a new event" => "Δημιουργήστε ένα νέο συμβάν", +"Share with:" => "Διαμοιρασμός με:", +"Shared with" => "Διαμοιράστηκε με", +"Unshare" => "Διακοπή Διαμοιρασμού", +"Nobody" => "Κανείς", +"Shared via calendar" => "Διαμοιράστηκε μέσω ημερολογίου", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "Σημείωση: Οι ενέργειες στα συμβάντα που διαμοιράζονται μέσω ημερολογίου θα επηρεάσουν όλο το ημερολόγιο που διαμοιράζεται.", +"View an event" => "Εμφάνισε ένα συμβάν", +"No categories selected" => "Δεν επελέγησαν κατηγορίες", +"of" => "του", +"at" => "στο", +"General" => "Γενικά", +"Timezone" => "Ζώνη ώρας", +"Update timezone automatically" => "Αυτόματη ενημέρωση ζώνης ώρας", +"Time format" => "Μορφή ώρας", +"24h" => "24ω", +"12h" => "12ω", +"Start week on" => "Πρώτη μέρα της εβδομάδας", +"Cache" => "Cache", +"Clear cache for repeating events" => "Εκκαθάριση λανθάνουσας μνήμης για επανάληψη συμβάντων", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Διευθύνσεις συγχρονισμού ημερολογίου CalDAV", +"more info" => "περισσότερες πλροφορίες", +"Primary address (Kontact et al)" => "Κύρια Διεύθυνση(Επαφή και άλλα)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => " iCalendar link(s) μόνο για ανάγνωση" +); diff --git a/l10n/eo.php b/l10n/eo.php new file mode 100644 index 000000000..8dd550633 --- /dev/null +++ b/l10n/eo.php @@ -0,0 +1,210 @@ + "Ne ĉiuj kalendaroj estas tute kaŝmemorigitaj", +"Everything seems to be completely cached" => "Ĉio ŝajnas tute kaŝmemorigita", +"No calendars found." => "Neniu kalendaro troviĝis.", +"No events found." => "Neniu okazaĵo troviĝis.", +"Wrong calendar" => "Malĝusta kalendaro", +"You do not have the permissions to edit this event." => "Vi ne havas la permeson redakti ĉi tiun okazaĵon.", +"The file contained either no events or all events are already saved in your calendar." => "Aŭ la dosiero enhavas neniun okazaĵon aŭ ĉiuj okazaĵoj jam estas konservitaj en via kalendaro.", +"events has been saved in the new calendar" => "okazaĵoj estas konservitaj en la nova kalendaro", +"Import failed" => "Enporto malsukcesis", +"events has been saved in your calendar" => "okazaĵoj estas konservitaj en via kalendaro", +"New Timezone:" => "Nova horozono:", +"Timezone changed" => "La horozono estas ŝanĝita", +"Invalid request" => "Nevalida peto", +"Calendar" => "Kalendaro", +"Deletion failed" => "Forigo malsukcesis", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd, la d-a de MMMM[ yyyy]{ – [ddd, la d-a de] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd, la d-a de MMMM[ yyyy] HH:mm{ – [ddd, la d-a de MMMM yyyy] HH:mm}", +"user" => "uzanto", +"group" => "grupo", +"Editable" => "Redaktebla", +"Shareable" => "Kunhavigebla", +"Deletable" => "Forigebla", +"ddd" => "ddd", +"ddd M/d" => "ddd d/M", +"dddd M/d" => "dddd d/M", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "d MMM[ yyyy]{ '—'d[ MMM] yyyy}", +"dddd, MMM d, yyyy" => "dddd, la d-a de MMM yyyy", +"Sunday" => "dimanĉo", +"Monday" => "lundo", +"Tuesday" => "mardo", +"Wednesday" => "merkredo", +"Thursday" => "ĵaŭdo", +"Friday" => "vendredo", +"Saturday" => "sabato", +"Sun." => "dim.", +"Mon." => "lun.", +"Tue." => "mar.", +"Wed." => "mer.", +"Thu." => "ĵaŭ.", +"Fri." => "ven.", +"Sat." => "sab.", +"January" => "Januaro", +"February" => "Februaro", +"March" => "Marto", +"April" => "Aprilo", +"May" => "Majo", +"June" => "Junio", +"July" => "Julio", +"August" => "Aŭgusto", +"September" => "Septembro", +"October" => "Oktobro", +"November" => "Novembro", +"December" => "Decembro", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Apr.", +"May." => "Maj.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aŭg.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dec.", +"All day" => "La tuta tago", +"New Calendar" => "Nova kalendaro", +"Missing or invalid fields" => "Estas mankantaj aŭ nevalidaj kampoj", +"Title" => "Titolo", +"From Date" => "ekde la dato", +"From Time" => "ekde la horo", +"To Date" => "ĝis la dato", +"To Time" => "ĝis la horo", +"The event ends before it starts" => "La okazaĵo finas antaŭ komenci", +"There was a database fail" => "Datumbaza malsukceso okazis", +"Birthday" => "Naskiĝotago", +"Business" => "Negoco", +"Call" => "Voko", +"Clients" => "Klientoj", +"Deliverer" => "Livero", +"Holidays" => "Ferioj", +"Ideas" => "Ideoj", +"Journey" => "Vojaĝo", +"Jubilee" => "Jubileo", +"Meeting" => "Rendevuo", +"Other" => "Alia", +"Personal" => "Persona", +"Projects" => "Projektoj", +"Questions" => "Demandoj", +"Work" => "Laboro", +"by" => "de", +"unnamed" => "nenomita", +"You do not have the permissions to update this calendar." => "Vi ne havas la permeson ĝisdatigi ĉi tiun kalendaron.", +"You do not have the permissions to delete this calendar." => "Vi ne havas la permeson forigi ĉi tiun kalendaron.", +"You do not have the permissions to add to this calendar." => "Vi ne havas la permeson aldoni al ĉi tiu kalendaro.", +"You do not have the permissions to add events to this calendar." => "Vi ne havas la permeson aldoni okazaĵojn al ĉi tiu kalendaro.", +"You do not have the permissions to delete this event." => "Vi ne havas la permeson forigi ĉi tiun okazaĵon.", +"Does not repeat" => "Ĉi tio ne ripetiĝas", +"Daily" => "Tage", +"Weekly" => "Semajne", +"Every Weekday" => "Labortage", +"Bi-Weekly" => "Semajnduope", +"Monthly" => "Monate", +"Yearly" => "Jare", +"never" => "neniam", +"by occurrences" => "laŭ aperoj", +"by date" => "laŭ dato", +"by monthday" => "laŭ monattago", +"by weekday" => "laŭ semajntago", +"events week of month" => "la monatsemajno de la okazaĵo", +"first" => "unua", +"second" => "dua", +"third" => "tria", +"fourth" => "kvara", +"fifth" => "kvina", +"last" => "lasta", +"by events date" => "laŭ okazaĵdato", +"by yearday(s)" => "laŭ jartago(j)", +"by weeknumber(s)" => "laŭ semajnnumero(j)", +"by day and month" => "laŭ tago kaj monato", +"Date" => "Dato", +"Cal." => "Kal.", +"Week" => "Semajno", +"Month" => "Monato", +"List" => "Listo", +"Today" => "Hodiaŭ", +"Settings" => "Agordo", +"Your calendars" => "Viaj kalendaroj", +"CalDav Link" => "CalDav-a ligilo", +"Share Calendar" => "Kunhavigi kalendaron", +"Download" => "Elŝuti", +"Edit" => "Redakti", +"Delete" => "Forigi", +"New calendar" => "Nova kalendaro", +"Edit calendar" => "Redakti la kalendaron", +"Displayname" => "Montrota nomo", +"Active" => "Aktiva", +"Calendar color" => "Kalendarokoloro", +"Save" => "Konservi", +"Submit" => "Sendi", +"Cancel" => "Nuligi", +"Edit an event" => "Redakti okazaĵon", +"Export" => "Elporti", +"Eventinfo" => "Informo de okazaĵo", +"Repeating" => "Ripetata", +"Alarm" => "Alarmo", +"Attendees" => "Ĉeestontoj", +"Share" => "Kunhavigi", +"Title of the Event" => "Okazaĵotitolo", +"Category" => "Kategorio", +"Separate categories with commas" => "Disigi kategoriojn per komoj", +"Edit categories" => "Redakti kategoriojn", +"All Day Event" => "La tuta tago", +"From" => "Ekde", +"To" => "Ĝis", +"Advanced options" => "Altnivela agordo", +"Location" => "Loko", +"Location of the Event" => "Loko de okazaĵo", +"Description" => "Priskribo", +"Description of the Event" => "Okazaĵopriskribo", +"Repeat" => "Ripeti", +"Advanced" => "Altnivelo", +"Select weekdays" => "Elekti semajntagojn", +"Select days" => "Elekti tagojn", +"and the events day of year." => "kaj la jartago de la okazaĵo.", +"and the events day of month." => "kaj la monattago de la okazaĵo.", +"Select months" => "Elekti monatojn", +"Select weeks" => "Elekti semajnojn", +"and the events week of year." => "kaj la jarsemajno de la okazaĵo.", +"Interval" => "Intervalo", +"End" => "Fino", +"occurrences" => "aperoj", +"create a new calendar" => "Krei novan kalendaron", +"Import a calendar file" => "Enporti kalendarodosieron", +"Please choose a calendar" => "Bonvolu elekti kalendaron", +"Name of new calendar" => "Nomo de la nova kalendaro", +"Take an available name!" => "Prenu haveblan nomon!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Kalendaro kun ĉi tiu nomo jam ekzastas. Se vi malgraŭe daŭros, ĉi tiuj kalendaroj kunfandiĝos.", +"Remove all events from the selected calendar" => "Forigi ĉiujn okazaĵojn el la elektita kalendaro", +"Import" => "Enporti", +"Close Dialog" => "Fermi la dialogon", +"Create a new event" => "Krei okazaĵon", +"Share with:" => "Kunhavigi kun:", +"Shared with" => "Kunhavigita kun", +"Unshare" => "Malkunhavigi", +"Nobody" => "Neniu", +"Shared via calendar" => "Kunhavigita per kalendaro", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTO: agoj sur okazaĵoj kunhavigitaj perkalendare influos la tutan kunhavigon de la kalendaro.", +"View an event" => "Vidi okazaĵon", +"No categories selected" => "Neniu kategorio elektita", +"of" => "de", +"at" => "ĉe", +"General" => "Ĝenerala", +"Timezone" => "Horozono", +"Update timezone automatically" => "Aŭtomate ĝisdatigi la horozonon", +"Time format" => "Horoformo", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Komenci semajnon je", +"Cache" => "Kaŝmemoro", +"Clear cache for repeating events" => "Forviŝi kaŝmemoron por ripeto de okazaĵoj", +"URLs" => "URL-oj", +"Calendar CalDAV syncing addresses" => "sinkronigaj adresoj por CalDAV-kalendaroj", +"more info" => "pli da informo", +"Primary address (Kontact et al)" => "Ĉefa adreso (Kontact kaj aliaj)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Nurlegebla(j) iCalendar-ligilo(j)" +); diff --git a/l10n/es.php b/l10n/es.php new file mode 100644 index 000000000..4f902d89d --- /dev/null +++ b/l10n/es.php @@ -0,0 +1,215 @@ + "Aún no se han guardado en caché todos los calendarios", +"Everything seems to be completely cached" => "Parece que se ha guardado todo en caché", +"No calendars found." => "No se encontraron calendarios.", +"No events found." => "No se encontraron eventos.", +"Wrong calendar" => "Calendario incorrecto", +"You do not have the permissions to edit this event." => "No tiene permisos para editar este evento.", +"The file contained either no events or all events are already saved in your calendar." => "El archivo no contiene eventos o ya existen en tu calendario.", +"events has been saved in the new calendar" => "Los eventos han sido guardados en el nuevo calendario", +"Import failed" => "Fallo en la importación", +"events has been saved in your calendar" => "eventos se han guardado en tu calendario", +"New Timezone:" => "Nueva zona horaria:", +"Timezone changed" => "Zona horaria cambiada", +"Invalid request" => "Petición no válida", +"Calendar" => "Calendario", +"Deletion failed" => "Falló el borrado", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ -[ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ -[ ddd d MMMM yyyy] HH:mm}", +"user" => "usuario", +"group" => "grupo", +"Editable" => "Editable", +"Shareable" => "Compartible", +"Deletable" => "Borrable", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Domingo", +"Monday" => "Lunes", +"Tuesday" => "Martes", +"Wednesday" => "Miércoles", +"Thursday" => "Jueves", +"Friday" => "Viernes", +"Saturday" => "Sábado", +"Sun." => "Dom.", +"Mon." => "Lun.", +"Tue." => "Mar.", +"Wed." => "Mier.", +"Thu." => "Jue.", +"Fri." => "Vie.", +"Sat." => "Sab.", +"January" => "Enero", +"February" => "Febrero", +"March" => "Marzo", +"April" => "Abril", +"May" => "Mayo", +"June" => "Junio", +"July" => "Julio", +"August" => "Agosto", +"September" => "Septiembre", +"October" => "Octubre", +"November" => "Noviembre", +"December" => "Diciembre", +"Jan." => "Ene.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Abr.", +"May." => "May.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Ago.", +"Sep." => "Sep.", +"Oct." => "Oct.", +"Nov." => "Nov.", +"Dec." => "Dic.", +"All day" => "Todo el día", +"New Calendar" => "Nuevo calendario", +"Missing or invalid fields" => "Campos inválidos o faltantes", +"Title" => "Título", +"From Date" => "Desde la fecha", +"From Time" => "Desde la hora", +"To Date" => "Hasta la fecha", +"To Time" => "Hasta la hora", +"The event ends before it starts" => "El evento termina antes de que comience", +"There was a database fail" => "Se ha producido un error en la base de datos", +"Birthday" => "Cumpleaños", +"Business" => "Negocios", +"Call" => "Llamada", +"Clients" => "Clientes", +"Deliverer" => "Entrega", +"Holidays" => "Festivos", +"Ideas" => "Ideas", +"Journey" => "Viaje", +"Jubilee" => "Aniversario", +"Meeting" => "Reunión", +"Other" => "Otro", +"Personal" => "Personal", +"Projects" => "Proyectos", +"Questions" => "Preguntas", +"Work" => "Trabajo", +"by" => "por", +"unnamed" => "Sin nombre", +"You do not have the permissions to update this calendar." => "No tiene permisos para actualizar este calendario.", +"You do not have the permissions to delete this calendar." => "No tiene permisos para eliminar este calendario.", +"You do not have the permissions to add to this calendar." => "No tiene permisos para añadir a este calendario.", +"You do not have the permissions to add events to this calendar." => "No tiene permisos para añadir eventos a este calendario.", +"You do not have the permissions to delete this event." => "No tiene permisos para eliminar este evento.", +"Busy" => "Ocupado", +"Public" => "Publico", +"Private" => "Privado", +"Confidential" => "Confidencial", +"Does not repeat" => "No se repite", +"Daily" => "Diariamente", +"Weekly" => "Semanalmente", +"Every Weekday" => "Días de semana laboral", +"Bi-Weekly" => "Cada 2 semanas", +"Monthly" => "Mensualmente", +"Yearly" => "Anualmente", +"never" => "nunca", +"by occurrences" => "por ocurrencias", +"by date" => "por fecha", +"by monthday" => "por día del mes", +"by weekday" => "por día de la semana", +"events week of month" => "eventos de la semana del mes", +"first" => "primer", +"second" => "segundo", +"third" => "tercer", +"fourth" => "cuarto", +"fifth" => "quinto", +"last" => "último", +"by events date" => "por fecha de los eventos", +"by yearday(s)" => "por día(s) del año", +"by weeknumber(s)" => "por número(s) de semana", +"by day and month" => "por día y mes", +"Date" => "Fecha", +"Cal." => "Cal.", +"Week" => "Semana", +"Month" => "Mes", +"List" => "Lista", +"Today" => "Hoy", +"Settings" => "Ajustes", +"Your calendars" => "Tus calendarios", +"CalDav Link" => "Enlace a CalDav", +"Share Calendar" => "Compartir calendario", +"Download" => "Descargar", +"Edit" => "Editar", +"Delete" => "Eliminar", +"New calendar" => "Nuevo calendario", +"Edit calendar" => "Editar calendario", +"Displayname" => "Nombre", +"Active" => "Activo", +"Calendar color" => "Color del calendario", +"Save" => "Guardar", +"Submit" => "Guardar", +"Cancel" => "Cancelar", +"Edit an event" => "Editar un evento", +"Export" => "Exportar", +"Eventinfo" => "Información del evento", +"Repeating" => "Repetición", +"Alarm" => "Alarma", +"Attendees" => "Asistentes", +"Share" => "Compartir", +"Title of the Event" => "Título del evento", +"Category" => "Categoría", +"Separate categories with commas" => "Separar categorías con comas", +"Edit categories" => "Editar categorías", +"Access Class" => "Clase de acceso", +"All Day Event" => "Todo el día", +"From" => "Desde", +"To" => "Hasta", +"Advanced options" => "Opciones avanzadas", +"Location" => "Lugar", +"Location of the Event" => "Lugar del evento", +"Description" => "Descripción", +"Description of the Event" => "Descripción del evento", +"Repeat" => "Repetir", +"Advanced" => "Avanzado", +"Select weekdays" => "Seleccionar días de la semana", +"Select days" => "Seleccionar días", +"and the events day of year." => "y el día del año de los eventos.", +"and the events day of month." => "y el día del mes de los eventos.", +"Select months" => "Seleccionar meses", +"Select weeks" => "Seleccionar semanas", +"and the events week of year." => "y la semana del año de los eventos.", +"Interval" => "Intervalo", +"End" => "Fin", +"occurrences" => "ocurrencias", +"create a new calendar" => "Crear un nuevo calendario", +"Import a calendar file" => "Importar un archivo de calendario", +"Please choose a calendar" => "Por favor, escoge un calendario", +"Name of new calendar" => "Nombre del nuevo calendario", +"Take an available name!" => "¡Elige un nombre disponible!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Ya existe un calendario con este nombre. Si continúas, se combinarán los calendarios.", +"Remove all events from the selected calendar" => "Eliminar todos los eventos del calendario seleccionado", +"Import" => "Importar", +"Close Dialog" => "Cerrar diálogo", +"Create a new event" => "Crear un nuevo evento", +"Share with:" => "Compartir con:", +"Shared with" => "Compartir con", +"Unshare" => "Dejar de compartir", +"Nobody" => "Nadie", +"Shared via calendar" => "Compartir via calendario", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTA: las acciones sobre eventos compartidos a través del calendario afectarán a la compartición de todo el calendario.", +"View an event" => "Ver un evento", +"No categories selected" => "Ninguna categoría seleccionada", +"of" => "de", +"at" => "a las", +"General" => "General", +"Timezone" => "Zona horaria", +"Update timezone automatically" => "Actualizar zona horaria automáticamente", +"Time format" => "Formato de hora", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Comenzar semana el", +"Cache" => "Caché", +"Clear cache for repeating events" => "Limpiar caché de eventos recurrentes", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Direcciones de sincronización de calendario CalDAV:", +"more info" => "Más información", +"Primary address (Kontact et al)" => "Dirección principal (Kontact y otros)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Enlace(s) iCalendar de sólo lectura" +); diff --git a/l10n/es_AR.php b/l10n/es_AR.php new file mode 100644 index 000000000..11042e8c5 --- /dev/null +++ b/l10n/es_AR.php @@ -0,0 +1,215 @@ + "Todavía no se guardaron en caché todos los calendarios", +"Everything seems to be completely cached" => "Parece que se guardó todo en el caché", +"No calendars found." => "No se encontraron calendarios.", +"No events found." => "No se encontraron eventos.", +"Wrong calendar" => "Calendario incorrecto", +"You do not have the permissions to edit this event." => "No tenés permisos suficientes para editar este evento.", +"The file contained either no events or all events are already saved in your calendar." => "El archivo no contiene eventos o ya existen en tu calendario.", +"events has been saved in the new calendar" => "Los eventos fueros guardados en el nuevo calendario", +"Import failed" => "Error al importar", +"events has been saved in your calendar" => "los eventos fueron guardados en tu calendario", +"New Timezone:" => "Nueva zona horaria:", +"Timezone changed" => "Zona horaria cambiada", +"Invalid request" => "Pedido no válido", +"Calendar" => "Calendario", +"Deletion failed" => "Error al borrar", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "usuario", +"group" => "grupo", +"Editable" => "Editable", +"Shareable" => "Se puede compartir", +"Deletable" => "Se puede borrar", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Domingo", +"Monday" => "Lunes", +"Tuesday" => "Martes", +"Wednesday" => "Miércoles", +"Thursday" => "Jueves", +"Friday" => "Viernes", +"Saturday" => "Sábado", +"Sun." => "dom.", +"Mon." => "lun.", +"Tue." => "mar.", +"Wed." => "miér.", +"Thu." => "jue.", +"Fri." => "vie.", +"Sat." => "sáb.", +"January" => "enero", +"February" => "febrero", +"March" => "marzo", +"April" => "abril", +"May" => "mayo", +"June" => "junio", +"July" => "julio", +"August" => "agosto", +"September" => "septiembre", +"October" => "octubre", +"November" => "noviembre", +"December" => "diciembre", +"Jan." => "ene.", +"Feb." => "feb.", +"Mar." => "mar.", +"Apr." => "abr.", +"May." => "may.", +"Jun." => "jun.", +"Jul." => "jul.", +"Aug." => "ago.", +"Sep." => "sep.", +"Oct." => "oct.", +"Nov." => "nov.", +"Dec." => "dic.", +"All day" => "Todo el día", +"New Calendar" => "Calendario nuevo", +"Missing or invalid fields" => "Campos faltantes o inválidos", +"Title" => "Título", +"From Date" => "Desde la fecha", +"From Time" => "Desde la hora", +"To Date" => "Hasta la fecha", +"To Time" => "Hasta la hora", +"The event ends before it starts" => "El evento termina antes de comiezar", +"There was a database fail" => "Se ha producido un error en la base de datos", +"Birthday" => "Cumpleaños", +"Business" => "Negocios", +"Call" => "Llamada", +"Clients" => "Clientes", +"Deliverer" => "Despachante", +"Holidays" => "Feriados", +"Ideas" => "Ideas", +"Journey" => "Viaje", +"Jubilee" => "Aniversario", +"Meeting" => "Reunión", +"Other" => "Otro", +"Personal" => "Personal", +"Projects" => "Proyectos", +"Questions" => "Preguntas", +"Work" => "Trabajo", +"by" => "por", +"unnamed" => "sin nombre", +"You do not have the permissions to update this calendar." => "No tenés permisos suficientes para actualizar este calendario.", +"You do not have the permissions to delete this calendar." => "No tenés permisos suficientes para eliminar este calendario.", +"You do not have the permissions to add to this calendar." => "No tenés permisos suficientes para añadir a este calendario.", +"You do not have the permissions to add events to this calendar." => "No tenés permisos suficientes para añadir eventos a este calendario.", +"You do not have the permissions to delete this event." => "No tenés permisos suficientes para eliminar este evento.", +"Busy" => "Ocupado", +"Public" => "Publico", +"Private" => "Privado", +"Confidential" => "Confidencial", +"Does not repeat" => "No se repite", +"Daily" => "Diariamente", +"Weekly" => "Semanalmente", +"Every Weekday" => "Días laborables", +"Bi-Weekly" => "Cada 2 semanas", +"Monthly" => "Mensualmente", +"Yearly" => "Anualmente", +"never" => "nunca", +"by occurrences" => "por ocurrencias", +"by date" => "por fecha", +"by monthday" => "por día del mes", +"by weekday" => "por día de la semana", +"events week of month" => "eventos de la semana dentro del mes", +"first" => "primer", +"second" => "segundo", +"third" => "tercer", +"fourth" => "cuarto", +"fifth" => "quinto", +"last" => "último", +"by events date" => "por fecha de los eventos", +"by yearday(s)" => "por día(s) del año", +"by weeknumber(s)" => "por número(s) de semana", +"by day and month" => "por día y mes", +"Date" => "Fecha", +"Cal." => "Cal.", +"Week" => "Semana", +"Month" => "Mes", +"List" => "Lista", +"Today" => "Hoy", +"Settings" => "Ajustes", +"Your calendars" => "Tus calendarios", +"CalDav Link" => "Enlace a CalDav", +"Share Calendar" => "Compartir calendario", +"Download" => "Descargar", +"Edit" => "Editar", +"Delete" => "Borrar", +"New calendar" => "Calendario nuevo", +"Edit calendar" => "Editar calendario", +"Displayname" => "Nombre", +"Active" => "Activo", +"Calendar color" => "Color del calendario", +"Save" => "Guardar", +"Submit" => "Guardar", +"Cancel" => "Cancelar", +"Edit an event" => "Editar un evento", +"Export" => "Exportar", +"Eventinfo" => "Información del evento", +"Repeating" => "Repetición", +"Alarm" => "Alarma", +"Attendees" => "Asistentes", +"Share" => "Compartir", +"Title of the Event" => "Título del evento", +"Category" => "Categoría", +"Separate categories with commas" => "Separar categorías con comas", +"Edit categories" => "Editar categorías", +"Access Class" => "Clase de acceso", +"All Day Event" => "Todo el día", +"From" => "Desde", +"To" => "Hasta", +"Advanced options" => "Opciones avanzadas", +"Location" => "Lugar", +"Location of the Event" => "Lugar del evento", +"Description" => "Descripción", +"Description of the Event" => "Descripción del evento", +"Repeat" => "Repetir", +"Advanced" => "Avanzado", +"Select weekdays" => "Seleccionar días de la semana", +"Select days" => "Seleccionar días", +"and the events day of year." => "y el día del año de los eventos.", +"and the events day of month." => "y el día del mes de los eventos.", +"Select months" => "Seleccionar meses", +"Select weeks" => "Seleccionar semanas", +"and the events week of year." => "y la semana del año de los eventos.", +"Interval" => "Intervalo", +"End" => "Fin", +"occurrences" => "ocurrencias:", +"create a new calendar" => "Crear un nuevo calendario", +"Import a calendar file" => "Importar un archivo de calendario", +"Please choose a calendar" => "Elegí un calendario", +"Name of new calendar" => "Nombre del nuevo calendario", +"Take an available name!" => "¡Elegí un nombre que esté disponible!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Ya existe un calendario con este nombre. Si seguís, se combinarán los calendarios.", +"Remove all events from the selected calendar" => "Remover todos los eventos del calendario seleccionado ", +"Import" => "Importar", +"Close Dialog" => "Cerrar diálogo", +"Create a new event" => "Crear un evento nuevo", +"Share with:" => "Compartir con:", +"Shared with" => "Compartir con", +"Unshare" => "Dejar de compartir", +"Nobody" => "Nadie", +"Shared via calendar" => "Compartido a través del calendario", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "Nota: Acciones sobre eventos compartidos a través del calendario, afectan el la compartición del calendario en su totalidad", +"View an event" => "Ver un evento", +"No categories selected" => "No hay categorías seleccionada", +"of" => "de", +"at" => "a las", +"General" => "General", +"Timezone" => "Zona horaria", +"Update timezone automatically" => "Actualizar zona horaria automáticamente", +"Time format" => "Formato de hora", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "La semana comienza el", +"Cache" => "Caché", +"Clear cache for repeating events" => "Limpiar caché de eventos recurrentes", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Direcciones de sincronización de calendario CalDAV:", +"more info" => "más información", +"Primary address (Kontact et al)" => "Dirección principal (Kontact y semejantes)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Enlace(s) iCalendar de sólo lectura" +); diff --git a/l10n/et_EE.php b/l10n/et_EE.php new file mode 100644 index 000000000..64d6c9ad2 --- /dev/null +++ b/l10n/et_EE.php @@ -0,0 +1,215 @@ + "Kõik kalendrid pole täielikult puhverdatud", +"Everything seems to be completely cached" => "Kõik näib olevat puhverdatud", +"No calendars found." => "Kalendreid ei leitud.", +"No events found." => "Üritusi ei leitud.", +"Wrong calendar" => "Vale kalender", +"You do not have the permissions to edit this event." => "Sul pole õigusi selle ürituse muutmiseks.", +"The file contained either no events or all events are already saved in your calendar." => "Failis polnud ühtegi üritust või kõik üritused on juba sinu kalendrisse salvestatud.", +"events has been saved in the new calendar" => "üritust salvestati uude kalendrisse", +"Import failed" => "Importimine ebaõnnestus", +"events has been saved in your calendar" => "üritused on salvestatud sinu kalendrisse", +"New Timezone:" => "Uus ajavöönd:", +"Timezone changed" => "Ajavöönd on muudetud", +"Invalid request" => "Vigane päring", +"Calendar" => "Kalender", +"Deletion failed" => "Kustutamine ebaõnnestus", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "kasutaja", +"group" => "grupp", +"Editable" => "Muudetav", +"Shareable" => "Jagatav", +"Deletable" => "Kustutatav", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Pühapäev", +"Monday" => "Esmaspäev", +"Tuesday" => "Teisipäev", +"Wednesday" => "Kolmapäev", +"Thursday" => "Neljapäev", +"Friday" => "Reede", +"Saturday" => "Laupäev", +"Sun." => "P", +"Mon." => "E", +"Tue." => "T", +"Wed." => "K", +"Thu." => "N", +"Fri." => "R", +"Sat." => "L", +"January" => "Jaanuar", +"February" => "Veebruar", +"March" => "Märts", +"April" => "Aprill", +"May" => "Mai", +"June" => "Juuni", +"July" => "Juuli", +"August" => "August", +"September" => "September", +"October" => "Oktoober", +"November" => "November", +"December" => "Detsember", +"Jan." => "Jaan.", +"Feb." => "Veebr.", +"Mar." => "Mär.", +"Apr." => "Apr.", +"May." => "Mai", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Sep." => "Sept.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dets.", +"All day" => "Kogu päev", +"New Calendar" => "Uus kalender", +"Missing or invalid fields" => "Puuduvad või vigased väljad", +"Title" => "Pealkiri", +"From Date" => "Alates kuupäevast", +"From Time" => "Alates kellaajast", +"To Date" => "Kuni kuupäevani", +"To Time" => "Kuni kellaajani", +"The event ends before it starts" => "Üritus lõpeb enne, kui see algab", +"There was a database fail" => "Tekkis andmebaasi viga", +"Birthday" => "Sünnipäev", +"Business" => "Äri", +"Call" => "Helista", +"Clients" => "Kliendid", +"Deliverer" => "Kohaletoimetaja", +"Holidays" => "Pühad", +"Ideas" => "Ideed", +"Journey" => "Reis", +"Jubilee" => "Juubel", +"Meeting" => "Kohtumine", +"Other" => "Muu", +"Personal" => "Isiklik", +"Projects" => "Projektid", +"Questions" => "Küsimused", +"Work" => "Töö", +"by" => "lisas", +"unnamed" => "nimetu", +"You do not have the permissions to update this calendar." => "Sul pole õigusi selle kalendri uuendamiseks.", +"You do not have the permissions to delete this calendar." => "Sul pole õigusi selle kalendri kustutamiseks.", +"You do not have the permissions to add to this calendar." => "Sul pole õigusi selle kalendri lisamiseks.", +"You do not have the permissions to add events to this calendar." => "Sul pole õigusi sellesse kalendrisse ürituste lisamiseks.", +"You do not have the permissions to delete this event." => "Sul pole õigusi selle ürituse kustutamiseks.", +"Busy" => "Hõivatud", +"Public" => "Avalik", +"Private" => "Privaatne", +"Confidential" => "Konfitentsiaalne", +"Does not repeat" => "Ei kordu", +"Daily" => "Iga päev", +"Weekly" => "Iga nädal", +"Every Weekday" => "Igal nädalapäeval", +"Bi-Weekly" => "Üle nädala", +"Monthly" => "Igal kuul", +"Yearly" => "Igal aastal", +"never" => "mitte kunagi", +"by occurrences" => "toimumiskordade järgi", +"by date" => "kuupäeva järgi", +"by monthday" => "kuu päeva järgi", +"by weekday" => "nädalapäeva järgi", +"events week of month" => "ürituse kuu nädal", +"first" => "esimene", +"second" => "teine", +"third" => "kolmas", +"fourth" => "neljas", +"fifth" => "viies", +"last" => "viimane", +"by events date" => "ürituste kuupäeva järgi", +"by yearday(s)" => "aasta päeva(de) järgi", +"by weeknumber(s)" => "nädala numbri(te) järgi", +"by day and month" => "kuu ja päeva järgi", +"Date" => "Kuupäev", +"Cal." => "Kal.", +"Week" => "Nädal", +"Month" => "Kuu", +"List" => "Nimekiri", +"Today" => "Täna", +"Settings" => "Seaded", +"Your calendars" => "Sinu kalendrid", +"CalDav Link" => "CalDav Link", +"Share Calendar" => "Jaga kalendrit", +"Download" => "Lae alla", +"Edit" => "Muuda", +"Delete" => "Kustuta", +"New calendar" => "Uus kalender", +"Edit calendar" => "Muuda kalendrit", +"Displayname" => "Näidatav nimi", +"Active" => "Aktiivne", +"Calendar color" => "Kalendri värv", +"Save" => "Salvesta", +"Submit" => "OK", +"Cancel" => "Loobu", +"Edit an event" => "Muuda sündmust", +"Export" => "Ekspordi", +"Eventinfo" => "Ürituse info", +"Repeating" => "Kordamine", +"Alarm" => "Alarm", +"Attendees" => "Osalejad", +"Share" => "Jaga", +"Title of the Event" => "Sündmuse pealkiri", +"Category" => "Kategooria", +"Separate categories with commas" => "Eralda kategooriad komadega", +"Edit categories" => "Muuda kategooriaid", +"Access Class" => "Ligipääsu klass", +"All Day Event" => "Kogu päeva sündmus", +"From" => "Alates", +"To" => "Kuni", +"Advanced options" => "Lisavalikud", +"Location" => "Asukoht", +"Location of the Event" => "Sündmuse toimumiskoht", +"Description" => "Kirjeldus", +"Description of the Event" => "Sündmuse kirjeldus", +"Repeat" => "Korda", +"Advanced" => "Täpsem", +"Select weekdays" => "Vali nädalapäevad", +"Select days" => "Vali päevad", +"and the events day of year." => "ja ürituse päev aastas.", +"and the events day of month." => "ja ürituse päev kuus.", +"Select months" => "Vali kuud", +"Select weeks" => "Vali nädalad", +"and the events week of year." => "ja ürituse nädal aastas.", +"Interval" => "Intervall", +"End" => "Lõpp", +"occurrences" => "toimumiskordi", +"create a new calendar" => "loo uus kalender", +"Import a calendar file" => "Impordi kalendrifail", +"Please choose a calendar" => "Palun vali kalender", +"Name of new calendar" => "Uue kalendri nimi", +"Take an available name!" => "Võta saadaolev nimi!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Selle nimega kalender on juba olemas. Kui sa siiski jätkad, siis need kalendrid liidetakse.", +"Remove all events from the selected calendar" => "Eemalda valitud kalendrist kõik sündmused", +"Import" => "Impordi", +"Close Dialog" => "Sulge dialoogiaken", +"Create a new event" => "Loo sündmus", +"Share with:" => "Jaga kasutajaga:", +"Shared with" => "Jagatud kasutajaga", +"Unshare" => "Lõpeta jagamine", +"Nobody" => "Mitte keegi", +"Shared via calendar" => "Jaga läbi kalendri", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "MÄRKUS: Läbi selle kalendri jagatud üritused mõjutavad kogu kalendri jagamist.", +"View an event" => "Vaata üritust", +"No categories selected" => "Ühtegi kategooriat pole valitud", +"of" => "/", +"at" => "kell", +"General" => "Üldine", +"Timezone" => "Ajavöönd", +"Update timezone automatically" => "Uuenda ajavööndit automaatselt", +"Time format" => "Ajavorming", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Nädal algab päevaga", +"Cache" => "Puhver", +"Clear cache for repeating events" => "Tühjenda korduvate ürituste puhver", +"URLs" => "URL-id", +"Calendar CalDAV syncing addresses" => "Kalendri CalDAV sünkroniseerimise aadressid", +"more info" => "lisainfo", +"Primary address (Kontact et al)" => "Peamine aadress (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => " iCalendar lingid ainult loetavana" +); diff --git a/l10n/eu.php b/l10n/eu.php new file mode 100644 index 000000000..08cf9a761 --- /dev/null +++ b/l10n/eu.php @@ -0,0 +1,213 @@ + "Egutegi guztiak ez daude guztiz cacheatuta", +"Everything seems to be completely cached" => "Dena guztiz cacheatuta dagoela dirudi", +"No calendars found." => "Ez da egutegirik aurkitu.", +"No events found." => "Ez da gertaerarik aurkitu.", +"Wrong calendar" => "Egutegi okerra", +"You do not have the permissions to edit this event." => "Gertaera hau editatzeko baimenik ez daukazu.", +"The file contained either no events or all events are already saved in your calendar." => "Fitxategiak ez zuen gertaerarik edo gertaera guztiak dagoeneko egutegian gordeta zeuden.", +"events has been saved in the new calendar" => "gertaerak egutegi berrian gorde dira", +"Import failed" => "Inportazioak huts egin du", +"events has been saved in your calendar" => "gertaerak zure egutegian gorde dira", +"New Timezone:" => "Ordu-zonalde berria", +"Timezone changed" => "Ordu-zonaldea aldatuta", +"Invalid request" => "Baliogabeko eskaera", +"Calendar" => "Egutegia", +"Deletion failed" => "Ezabaketak huts egin du", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "[ yyyy] MMMM ddd d{ - yyyy MMMM [ddd d] }", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "[ yyyy] MMMM ddd d HH:mm{ - yyyy MMMM [ddd d] HH:mm}", +"user" => "erabiltzailea", +"group" => "taldea", +"Editable" => "Editagarria", +"Shareable" => "Partekagarria", +"Deletable" => "Ezabagarria", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "yyyy MMMM", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "[ yyyy] MMM d{ '—'yyyy [ MMM] d}", +"dddd, MMM d, yyyy" => "yyyy, MMM d, dddd", +"Sunday" => "Igandea", +"Monday" => "Astelehena", +"Tuesday" => "Asteartea", +"Wednesday" => "Asteazkena", +"Thursday" => "Osteguna", +"Friday" => "Ostirala", +"Saturday" => "Larunbata", +"Sun." => "ig.", +"Mon." => "al.", +"Tue." => "ar.", +"Wed." => "az.", +"Thu." => "og.", +"Fri." => "ol.", +"Sat." => "lr.", +"January" => "Urtarrila", +"February" => "Otsaila", +"March" => "Martxoa", +"April" => "Apirila", +"May" => "Maiatza", +"June" => "Ekaina", +"July" => "Uztaila", +"August" => "Abuztua", +"September" => "Iraila", +"October" => "Urria", +"November" => "Azaroa", +"December" => "Abendua", +"Jan." => "urt.", +"Feb." => "ots.", +"Mar." => "mar.", +"Apr." => "api.", +"May." => "mai.", +"Jun." => "eka.", +"Jul." => "uzt.", +"Aug." => "abu.", +"Sep." => "ira.", +"Oct." => "urr.", +"Nov." => "aza.", +"Dec." => "abe.", +"All day" => "Egun guztia", +"New Calendar" => "Egutegi berria", +"Missing or invalid fields" => "Eremuak falta dira edo gaizki daude", +"Title" => "Izenburua", +"From Date" => "Hasierako Data", +"From Time" => "Hasierako Ordua", +"To Date" => "Bukaerako Data", +"To Time" => "Bukaerako Ordua", +"The event ends before it starts" => "Gertaera hasi baino lehen bukatzen da", +"There was a database fail" => "Datu-baseak huts egin du", +"Birthday" => "Jaioteguna", +"Business" => "Negozioa", +"Call" => "Deia", +"Clients" => "Bezeroak", +"Deliverer" => "Banatzailea", +"Holidays" => "Oporrak", +"Ideas" => "Ideiak", +"Journey" => "Bidaia", +"Jubilee" => "Urteurrena", +"Meeting" => "Bilera", +"Other" => "Bestelakoa", +"Personal" => "Pertsonala", +"Projects" => "Proiektuak", +"Questions" => "Galderak", +"Work" => "Lana", +"unnamed" => "izengabea", +"You do not have the permissions to update this calendar." => "Egutegi hau eguneratzeko baimenik ez daukazu.", +"You do not have the permissions to delete this calendar." => "Egutegi hau ezabatzeko baimenik ez daukazu.", +"You do not have the permissions to add to this calendar." => "Egutegi honetan gehitzeko baimenik ez daukazu.", +"You do not have the permissions to add events to this calendar." => "Egutegi honetan gertaerak gehitzeko baimenik ez daukazu.", +"You do not have the permissions to delete this event." => "Gertaera hau ezabatzeko baimenik ez daukazu.", +"Busy" => "Lanpetuta", +"Public" => "Publikoa", +"Private" => "Pribatua", +"Confidential" => "Konfidentziala", +"Does not repeat" => "Ez da errepikatzen", +"Daily" => "Egunero", +"Weekly" => "Astero", +"Every Weekday" => "Asteko egun guztietan", +"Bi-Weekly" => "Bi-Astero", +"Monthly" => "Hilabetero", +"Yearly" => "Urtero", +"never" => "inoiz", +"by occurrences" => "errepikapen kopuruagatik", +"by date" => "dataren arabera", +"by monthday" => "hileko egunaren arabera", +"by weekday" => "asteko egunaren arabera", +"events week of month" => "gertaeraren hilabeteko astea", +"first" => "lehenengoa", +"second" => "bigarrean", +"third" => "hirugarrena", +"fourth" => "laugarrena", +"fifth" => "bostgarrena", +"last" => "azkena", +"by events date" => "gertaeren dataren arabera", +"by yearday(s)" => "urteko egunaren arabera", +"by weeknumber(s)" => "aste zenbaki(ar)en arabera", +"by day and month" => "eguna eta hilabetearen arabera", +"Date" => "Data", +"Cal." => "Eg.", +"Week" => "Astea", +"Month" => "Hilabetea", +"List" => "Zerrenda", +"Today" => "Gaur", +"Settings" => "Ezarpenak", +"Your calendars" => "Zure egutegiak", +"CalDav Link" => "CalDav lotura", +"Share Calendar" => "Elkarbanatu egutegia", +"Download" => "Deskargatu", +"Edit" => "Editatu", +"Delete" => "Ezabatu", +"New calendar" => "Egutegi berria", +"Edit calendar" => "Editatu egutegia", +"Displayname" => "Bistaratzeko izena", +"Active" => "Aktiboa", +"Calendar color" => "Egutegiaren kolorea", +"Save" => "Gorde", +"Submit" => "Bidali", +"Cancel" => "Ezeztatu", +"Edit an event" => "Editatu gertaera", +"Export" => "Exportatu", +"Eventinfo" => "Gertaeraren informazioa", +"Repeating" => "Errepikapena", +"Alarm" => "Alarma", +"Attendees" => "Partaideak", +"Share" => "Elkarbanatu", +"Title of the Event" => "Gertaeraren izenburua", +"Category" => "Kategoria", +"Separate categories with commas" => "Banatu kategoriak komekin", +"Edit categories" => "Editatu kategoriak", +"Access Class" => "Sarrera mota", +"All Day Event" => "Egun osoko gertaera", +"From" => "Hasiera", +"To" => "Bukaera", +"Advanced options" => "Aukera aurreratuak", +"Location" => "Kokalekua", +"Location of the Event" => "Gertaeraren kokalekua", +"Description" => "Deskribapena", +"Description of the Event" => "Gertaeraren deskribapena", +"Repeat" => "Errepikatu", +"Advanced" => "Aurreratua", +"Select weekdays" => "Hautatu asteko egunak", +"Select days" => "Hautatu egunak", +"and the events day of year." => "eta gertaeraren urteko eguna.", +"and the events day of month." => "eta gertaeraren hilabeteko eguna.", +"Select months" => "Hautatu hilabeteak", +"Select weeks" => "Hautatu asteak", +"and the events week of year." => "eta gertaeraren urteko astea.", +"Interval" => "Tartea", +"End" => "Amaiera", +"occurrences" => "errepikapenak", +"create a new calendar" => "sortu egutegi berria", +"Import a calendar file" => "Inportatu egutegi fitxategi bat", +"Please choose a calendar" => "Mesedez aukeratu egutegi bat.", +"Name of new calendar" => "Egutegi berriaren izena", +"Take an available name!" => "Hartu eskuragarri dagoen izen bat!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Izen hau duen egutegi bat dagoeneko existitzen da. Hala ere jarraitzen baduzu, egutegi hauek elkartuko dira.", +"Remove all events from the selected calendar" => "Ezabatu hautatutako egutegiko gertaera guztiak", +"Import" => "Importatu", +"Close Dialog" => "Itxi lehioa", +"Create a new event" => "Sortu gertaera berria", +"Share with:" => "elkarbanatu honekin:", +"Shared with" => "Hauekin elkarbanatuta:", +"Unshare" => "Ez elkarbanatu", +"Nobody" => "Inor", +"Shared via calendar" => "Egutegiaren bidez elkarbanatuta", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "OHARRA: Egutegiaren bidez elkarbanatutako gertaeren gaineko ekintzak egutegi osoaren elkarbanatzean izango dute eragina.", +"View an event" => "Ikusi gertaera bat", +"No categories selected" => "Ez da kategoriarik hautatu", +"at" => "ordua", +"General" => "Orokorra", +"Timezone" => "Ordu-zonaldea", +"Update timezone automatically" => "Eguneratu ordu-zonaldea automatikoki", +"Time format" => "Ordu formatua", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Astea hasteko eguna", +"Cache" => "Cache", +"Clear cache for repeating events" => "Ezabatu gertaera errepikakorren cachea", +"URLs" => "URLak", +"Calendar CalDAV syncing addresses" => "Egutegiaren CalDAV sinkronizazio helbideak", +"more info" => "informazio gehiago", +"Primary address (Kontact et al)" => "Helbide nagusia", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Bakarrik irakurtzeko iCalendar lotura(k)" +); diff --git a/l10n/fa.php b/l10n/fa.php new file mode 100644 index 000000000..73ccb4770 --- /dev/null +++ b/l10n/fa.php @@ -0,0 +1,215 @@ + "تمامی تقویم ها ذخیره ی موقتی نشده اند", +"Everything seems to be completely cached" => "همه چیز ذخیره ی موقتی شد.", +"No calendars found." => "هیچ تقویمی پیدا نشد", +"No events found." => "هیچ رویدادی پیدا نشد", +"Wrong calendar" => "تقویم اشتباه", +"You do not have the permissions to edit this event." => "شما دسترسی مجاز برای اصلاح این رویداد را ندارید.", +"The file contained either no events or all events are already saved in your calendar." => "فایل شامل همه یا هیچ یک از رویدادها هم اکنون در تقویم شما ذخیره شده است.", +"events has been saved in the new calendar" => "رویدادها در تقویم جدید ذخیره شد.", +"Import failed" => "خطای وارد کردن", +"events has been saved in your calendar" => "رویدادها در تقویم شما ذخیره شده است", +"New Timezone:" => "زمان محلی جدید", +"Timezone changed" => "زمان محلی تغییر یافت", +"Invalid request" => "درخواست نامعتبر", +"Calendar" => "تقویم", +"Deletion failed" => "حذف کردن انجام نشد", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "کاربر", +"group" => "گروه", +"Editable" => "قابل ویرایش", +"Shareable" => "قابل به اشتراک گذاری", +"Deletable" => "قابل حذف", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "DDD m[ yyyy]{ '—'[ DDD] m yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "یکشنبه", +"Monday" => "دوشنبه", +"Tuesday" => "سه شنبه", +"Wednesday" => "چهارشنبه", +"Thursday" => "پنجشنبه", +"Friday" => "جمعه", +"Saturday" => "شنبه", +"Sun." => "یکشنبه", +"Mon." => "دوشنبه", +"Tue." => "سه شنبه", +"Wed." => "چهارشنبه", +"Thu." => "پنج شنبه", +"Fri." => "جمعه", +"Sat." => "شنبه", +"January" => "ژانویه", +"February" => "فبریه", +"March" => "مارس", +"April" => "آوریل", +"May" => "می", +"June" => "ژوءن", +"July" => "جولای", +"August" => "آگوست", +"September" => "سپتامبر", +"October" => "اکتبر", +"November" => "نوامبر", +"December" => "دسامبر", +"Jan." => "ژانویه", +"Feb." => "فوریه", +"Mar." => "مارچ", +"Apr." => "آوریل", +"May." => "می", +"Jun." => "ژوئن", +"Jul." => "جولای", +"Aug." => "آگوست", +"Sep." => "سپتامبر", +"Oct." => "اکتبر", +"Nov." => "نوامبر", +"Dec." => "دسامبر", +"All day" => "هرروز", +"New Calendar" => "تقویم جدید", +"Missing or invalid fields" => "فیلدهای گم شده یا نامعتبر", +"Title" => "عنوان", +"From Date" => "از تاریخ", +"From Time" => "از ساعت", +"To Date" => "به تاریخ", +"To Time" => "به ساعت", +"The event ends before it starts" => "رویداد قبل از شروع شدن تمام شده!", +"There was a database fail" => "یک پایگاه داده فرو مانده است", +"Birthday" => "روزتولد", +"Business" => "تجارت", +"Call" => "تماس گرفتن", +"Clients" => "مشتریان", +"Deliverer" => "نجات", +"Holidays" => "روزهای تعطیل", +"Ideas" => "ایده ها", +"Journey" => "سفر", +"Jubilee" => "سالگرد", +"Meeting" => "ملاقات", +"Other" => "دیگر", +"Personal" => "شخصی", +"Projects" => "پروژه ها", +"Questions" => "سوالات", +"Work" => "کار", +"by" => "با", +"unnamed" => "نام گذاری نشده", +"You do not have the permissions to update this calendar." => "شما دسترسی مجاز برای به روزرسانی این تقویم را ندارید.", +"You do not have the permissions to delete this calendar." => "شما دسترسی مجاز برای حذف این تقویم را ندارید.", +"You do not have the permissions to add to this calendar." => "شما دسترسی مجاز برای ایجاد این تقویم را ندارید.", +"You do not have the permissions to add events to this calendar." => "شما دسترسی مجاز برای افزودن رویداد به این تقویم را ندارید.", +"You do not have the permissions to delete this event." => "شما دسترسی مجاز برای حذف این رویداد را ندارید.", +"Busy" => "درحال کار", +"Public" => "عمومی", +"Private" => "خصوصی", +"Confidential" => "محرمانه", +"Does not repeat" => "تکرار نکنید", +"Daily" => "روزانه", +"Weekly" => "هفتهگی", +"Every Weekday" => "هرروز هفته", +"Bi-Weekly" => "دوهفته", +"Monthly" => "ماهانه", +"Yearly" => "سالانه", +"never" => "هرگز", +"by occurrences" => "به وسیله ظهور", +"by date" => "به وسیله تاریخ", +"by monthday" => "به وسیله روزهای ماه", +"by weekday" => "به وسیله روز های هفته", +"events week of month" => "رویداد های هفته هایی از ماه", +"first" => "اولین", +"second" => "دومین", +"third" => "سومین", +"fourth" => "چهارمین", +"fifth" => "پنجمین", +"last" => "آخرین", +"by events date" => "به وسیله رویداد های روزانه", +"by yearday(s)" => "به وسیله روز های سال(ها)", +"by weeknumber(s)" => "به وسیله شماره هفته(ها)", +"by day and month" => "به وسیله روز و ماه", +"Date" => "تاریخ", +"Cal." => "تقویم.", +"Week" => "هفته", +"Month" => "ماه", +"List" => "فهرست", +"Today" => "امروز", +"Settings" => "تنظیمات", +"Your calendars" => "تقویم های شما", +"CalDav Link" => "CalDav Link", +"Share Calendar" => "تقویم را به اشتراک بگذارید", +"Download" => "بارگیری", +"Edit" => "ویرایش", +"Delete" => "پاک کردن", +"New calendar" => "تقویم جدید", +"Edit calendar" => "ویرایش تقویم", +"Displayname" => "نام برای نمایش", +"Active" => "فعال", +"Calendar color" => "رنگ تقویم", +"Save" => "ذخیره سازی", +"Submit" => "ارسال", +"Cancel" => "انصراف", +"Edit an event" => "ویرایش رویداد", +"Export" => "خروجی گرفتن", +"Eventinfo" => "اطلاعات رویداد", +"Repeating" => "در حال تکرار کردن", +"Alarm" => "هشدار", +"Attendees" => "شرکت کنندگان", +"Share" => "به اشتراک گذاردن", +"Title of the Event" => "عنوان رویداد", +"Category" => "نوع", +"Separate categories with commas" => "گروه ها را به وسیله درنگ نما از هم جدا کنید", +"Edit categories" => "ویرایش گروه", +"Access Class" => "کلاس دسترسی", +"All Day Event" => "رویداد های روزانه", +"From" => "از", +"To" => "به", +"Advanced options" => "تنظیمات حرفه ای", +"Location" => "منطقه", +"Location of the Event" => "منطقه رویداد", +"Description" => "توضیحات", +"Description of the Event" => "توضیحات درباره رویداد", +"Repeat" => "تکرار", +"Advanced" => "پیشرفته", +"Select weekdays" => "انتخاب روز های هفته ", +"Select days" => "انتخاب روز ها", +"and the events day of year." => "و رویداد های روز از سال", +"and the events day of month." => "و رویداد های روز از ماه", +"Select months" => "انتخاب ماه ها", +"Select weeks" => "انتخاب هفته ها", +"and the events week of year." => "و رویداد هفته ها از سال", +"Interval" => "فاصله", +"End" => "پایان", +"occurrences" => "ظهور", +"create a new calendar" => "یک تقویم جدید ایجاد کنید", +"Import a calendar file" => "یک پرونده حاوی تقویم وارد کنید", +"Please choose a calendar" => "لطفا تقویم را انتخاب کنید", +"Name of new calendar" => "نام تقویم جدید", +"Take an available name!" => "یک نام موجود را برگزینید.", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "تقویمی با این نام هم اکنون وجود دارد. اگر ادامه دهید، این تقویم ها ادغام خواهند شد.", +"Remove all events from the selected calendar" => "حذف تمامی رویدادها از تقویم انتخاب شده", +"Import" => "ورودی دادن", +"Close Dialog" => "بستن دیالوگ", +"Create a new event" => "یک رویداد ایجاد کنید", +"Share with:" => "به اشتراک گذاری با:", +"Shared with" => "به اشتراک گذاشته شده با", +"Unshare" => "لغو اشتراک", +"Nobody" => "هیچ کس", +"Shared via calendar" => "به اشتراک گذاشته شده از طریق تقویم", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "توجه: عملیات روی رویدادهای به اشتراک گذاشته شده از طریق تقویم، بر اشتراک گذاری تقویم موثر است.", +"View an event" => "دیدن یک رویداد", +"No categories selected" => "هیچ گروهی انتخاب نشده", +"of" => "از", +"at" => "در", +"General" => "عمومی", +"Timezone" => "زمان محلی", +"Update timezone automatically" => "به روز رسانی خودکار ساعت محلی", +"Time format" => "قالب زمان", +"24h" => "24 ساعت", +"12h" => "12 ساعت", +"Start week on" => "شروع هفته از", +"Cache" => "ذخیره ی موقتی", +"Clear cache for repeating events" => "ذخیره ی موقتی برای رویدادهای تکراری پاک شود", +"URLs" => "آدرس ها", +"Calendar CalDAV syncing addresses" => "آدرس های همگام سازی تقویم CalDAV", +"more info" => "اطلاعات بیشتر", +"Primary address (Kontact et al)" => "نشانی اولیه", +"iOS/OS X" => "iOS/OS X ", +"Read only iCalendar link(s)" => "اتصالات فقط خواندنی iCalendar" +); diff --git a/l10n/fi.php b/l10n/fi.php new file mode 100644 index 000000000..014042132 --- /dev/null +++ b/l10n/fi.php @@ -0,0 +1,4 @@ + "asetukset", +"Save" => "tallentaa" +); diff --git a/l10n/fi_FI.php b/l10n/fi_FI.php new file mode 100644 index 000000000..8ff612df5 --- /dev/null +++ b/l10n/fi_FI.php @@ -0,0 +1,213 @@ + "Kaikki kalenterit eivät ole täysin välitallennettu", +"Everything seems to be completely cached" => "Kaikki on ilmeisesti tallennettu välimuistiin", +"No calendars found." => "Kalentereita ei löytynyt", +"No events found." => "Tapahtumia ei löytynyt.", +"Wrong calendar" => "Väärä kalenteri", +"You do not have the permissions to edit this event." => "Käyttöoikeutesi eivät riitä tämän tapahtuman muokkaukseen.", +"The file contained either no events or all events are already saved in your calendar." => "Tiedosto ei joko sisältänyt tapahtumia tai vaihtoehtoisesti kaikki tapahtumat on jo tallennettu kalenteriisi.", +"events has been saved in the new calendar" => "tapahtumat on tallennettu uuteen kalenteriin", +"Import failed" => "Tuonti epäonnistui", +"events has been saved in your calendar" => "tapahtumaa on tallennettu kalenteriisi", +"New Timezone:" => "Uusi aikavyöhyke:", +"Timezone changed" => "Aikavyöhyke vaihdettu", +"Invalid request" => "Virheellinen pyyntö", +"Calendar" => "Kalenteri", +"Deletion failed" => "Poisto epäonnistui", +"user" => "käyttäjä", +"group" => "ryhmä", +"Editable" => "Muoktattava", +"Shareable" => "Jaettavissa", +"Deletable" => "Poistettavissa", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Sunnuntai", +"Monday" => "Maanantai", +"Tuesday" => "Tiistai", +"Wednesday" => "Keskiviikko", +"Thursday" => "Torstai", +"Friday" => "Perjantai", +"Saturday" => "Lauantai", +"Sun." => "Su", +"Mon." => "Ma", +"Tue." => "Ti", +"Wed." => "Ke", +"Thu." => "To", +"Fri." => "Pe", +"Sat." => "La", +"January" => "Tammikuu", +"February" => "Helmikuu", +"March" => "Maaliskuu", +"April" => "Huhtikuu", +"May" => "Toukokuu", +"June" => "Kesäkuu", +"July" => "Heinäkuu", +"August" => "Elokuu", +"September" => "Syyskuu", +"October" => "Lokakuu", +"November" => "Marraskuu", +"December" => "Joulukuu", +"Jan." => "Tammi", +"Feb." => "Helmi", +"Mar." => "Maalis", +"Apr." => "Huhti", +"May." => "Touko", +"Jun." => "Kesä", +"Jul." => "Heinä", +"Aug." => "Elo", +"Sep." => "Syys", +"Oct." => "Loka", +"Nov." => "Marras", +"Dec." => "Joulu", +"All day" => "Koko päivä", +"New Calendar" => "Uusi kalenteri", +"Missing or invalid fields" => "Puuttuvia tai virheellisiä kenttiä", +"Title" => "Otsikko", +"From Date" => "Päivämäärästä", +"From Time" => "Alkuaika", +"To Date" => "Päivämäärään", +"To Time" => "Loppuaika", +"The event ends before it starts" => "Tapahtuma päättyy ennen alkamistaan", +"There was a database fail" => "Tapahtui tietokantavirhe", +"Birthday" => "Syntymäpäivä", +"Business" => "Työ", +"Call" => "Ota yhteyttä", +"Clients" => "Asiakkaat", +"Deliverer" => "Toimittaja", +"Holidays" => "Vapaapäivät", +"Ideas" => "Ideat", +"Journey" => "Matkustus", +"Jubilee" => "Vuosipäivät", +"Meeting" => "Tapaamiset", +"Other" => "Muut", +"Personal" => "Henkilökohtainen", +"Projects" => "Projektit", +"Questions" => "Kysymykset", +"Work" => "Työ", +"by" => "toimesta:", +"unnamed" => "nimetön", +"You do not have the permissions to update this calendar." => "Käyttöoikeutesi eivät riitä tämän kalenterin päivittämiseen.", +"You do not have the permissions to delete this calendar." => "Käyttöoikeutesi eivät riitä tämän kalenterin poistamiseen.", +"You do not have the permissions to add to this calendar." => "Käyttöoikeutesi eivät riitä lisäämiseen tähän kalenteriin.", +"You do not have the permissions to add events to this calendar." => "Käyttöoikeutesi eivät riitä tapahtumien lisäämiseen tähän kalenteriin.", +"You do not have the permissions to delete this event." => "Käyttöoikeutesi eivät riitä tämän tapahtuman poistamiseen.", +"Busy" => "Kiireinen", +"Public" => "Julkinen", +"Private" => "Yksityinen", +"Confidential" => "Luottamuksellinen", +"Does not repeat" => "Ei toistoa", +"Daily" => "Päivittäin", +"Weekly" => "Viikottain", +"Every Weekday" => "Arkipäivisin", +"Bi-Weekly" => "Joka toinen viikko", +"Monthly" => "Kuukausittain", +"Yearly" => "Vuosittain", +"never" => "Ei koskaan", +"by occurrences" => "esiintymisten mukaan", +"by date" => "päivämäärän mukaan", +"by monthday" => "kuukaudenpäivän mukaan", +"by weekday" => "viikonpäivän mukaan", +"events week of month" => "tapahtumat kuukauden viikolla", +"first" => "ensimmäinen", +"second" => "toinen", +"third" => "kolmas", +"fourth" => "neljäs", +"fifth" => "viides", +"last" => "viimeinen", +"by events date" => "tapahtumien päivämäärän mukaan", +"by yearday(s)" => "vuoden päivän mukaan", +"by weeknumber(s)" => "viikkonumero(ide)n mukaan", +"by day and month" => "päivän ja kuukauden mukaan", +"Date" => "Päivämäärä", +"Cal." => "Kal.", +"Week" => "Viikko", +"Month" => "Kuukausi", +"List" => "Lista", +"Today" => "Tänään", +"Settings" => "Asetukset", +"Your calendars" => "Omat kalenterisi", +"CalDav Link" => "CalDav-linkki", +"Share Calendar" => "Jaa kalenteri", +"Download" => "Lataa", +"Edit" => "Muokkaa", +"Delete" => "Poista", +"New calendar" => "Uusi kalenteri", +"Edit calendar" => "Muokkaa kalenteria", +"Displayname" => "Kalenterin nimi", +"Active" => "Aktiivinen", +"Calendar color" => "Kalenterin väri", +"Save" => "Tallenna", +"Submit" => "Talleta", +"Cancel" => "Peru", +"Edit an event" => "Muokkaa tapahtumaa", +"Export" => "Vie", +"Eventinfo" => "Tapahtumatiedot", +"Repeating" => "Toisto", +"Alarm" => "Hälytys", +"Attendees" => "Osallistujat", +"Share" => "Jaa", +"Title of the Event" => "Tapahtuman otsikko", +"Category" => "Luokka", +"Separate categories with commas" => "Erota luokat pilkuilla", +"Edit categories" => "Muokkaa luokkia", +"Access Class" => "Pääsyluokka", +"All Day Event" => "Koko päivän tapahtuma", +"From" => "Alkaa", +"To" => "Päättyy", +"Advanced options" => "Tarkemmat asetukset", +"Location" => "Sijainti", +"Location of the Event" => "Tapahtuman sijainti", +"Description" => "Kuvaus", +"Description of the Event" => "Tapahtuman kuvaus", +"Repeat" => "Toisto", +"Advanced" => "Edistynyt", +"Select weekdays" => "Valitse viikonpäivät", +"Select days" => "Valitse päivät", +"and the events day of year." => "ja tapahtumien päivä vuodessa.", +"and the events day of month." => "ja tapahtumien päivä kuukaudessa.", +"Select months" => "Valitse kuukaudet", +"Select weeks" => "Valitse viikot", +"and the events week of year." => "ja tapahtumien viikko vuodessa.", +"Interval" => "Intervalli", +"End" => "Loppu", +"occurrences" => "esiintymät", +"create a new calendar" => "luo uusi kalenteri", +"Import a calendar file" => "Tuo kalenteritiedosto", +"Please choose a calendar" => "Valitse kalenteri", +"Name of new calendar" => "Uuden kalenterin nimi", +"Take an available name!" => "Ota nimi, joka on käytettävissä!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Samalla nimellä on jo olemassa kalenteri. Jos jatkat kaikesta huolimatta, kalenterit yhdistetään.", +"Remove all events from the selected calendar" => "Poista kaikki tapahtumat valitusta kalenterista", +"Import" => "Tuo", +"Close Dialog" => "Sulje ikkuna", +"Create a new event" => "Luo uusi tapahtuma", +"Share with:" => "Jaa:", +"Shared with" => "Jaettu", +"Unshare" => "Poista jakaminen", +"Nobody" => "Ei kukaan", +"Shared via calendar" => "Jaettu kalenterin kanssa", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "HUOMAA: Kalenterissa jaettujen tapahtumien toimenpiteet vaikuttavat koko kalenterin jakamiseen. ", +"View an event" => "Avaa tapahtuma", +"No categories selected" => "Luokkia ei ole valittu", +"of" => " ", +"at" => " ", +"General" => "Yleiset", +"Timezone" => "Aikavyöhyke", +"Update timezone automatically" => "Päivitä aikavyöhykkeet automaattisesti", +"Time format" => "Ajan näyttömuoto", +"24h" => "24 tuntia", +"12h" => "12 tuntia", +"Start week on" => "Viikon alkamispäivä", +"Cache" => "Välimuisti", +"Clear cache for repeating events" => "Tyhjennä välimuisti toistuvista tapahtumista", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Kalenterin CalDAV-synkronointiosoitteet", +"more info" => "lisätietoja", +"Primary address (Kontact et al)" => "Ensisijainen osoite (Kontact ja muut vastaavat)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "iCalendar linkit, jotka ovat vain lukuoikeuksilla" +); diff --git a/l10n/fr.php b/l10n/fr.php new file mode 100644 index 000000000..08156f1ed --- /dev/null +++ b/l10n/fr.php @@ -0,0 +1,215 @@ + "Tous les calendriers ne sont pas mis en cache", +"Everything seems to be completely cached" => "Tout semble être en cache", +"No calendars found." => "Aucun calendrier n'a été trouvé.", +"No events found." => "Aucun événement n'a été trouvé.", +"Wrong calendar" => "Mauvais calendrier", +"You do not have the permissions to edit this event." => "Vous n'avez pas les droits suffisants pour éditer cet événement.", +"The file contained either no events or all events are already saved in your calendar." => "Soit le fichier ne contient aucun événement soit tous les événements sont déjà enregistrés dans votre calendrier.", +"events has been saved in the new calendar" => "Les événements ont été enregistrés dans le nouveau calendrier", +"Import failed" => "Échec de l'import", +"events has been saved in your calendar" => "Les événements ont été enregistrés dans votre calendrier", +"New Timezone:" => "Nouveau fuseau horaire :", +"Timezone changed" => "Fuseau horaire modifié", +"Invalid request" => "Requête invalide", +"Calendar" => "Calendrier", +"Deletion failed" => "La suppression a échoué", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "utilisateur", +"group" => "groupe", +"Editable" => "Modifiable", +"Shareable" => "Partageable", +"Deletable" => "Effaçable", +"ddd" => "ddd", +"ddd M/d" => "ddd d/M", +"dddd M/d" => "dddd d/M", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, d MMM, yyyy", +"Sunday" => "Dimanche", +"Monday" => "Lundi", +"Tuesday" => "Mardi", +"Wednesday" => "Mercredi", +"Thursday" => "Jeudi", +"Friday" => "Vendredi", +"Saturday" => "Samedi", +"Sun." => "Dim.", +"Mon." => "Lun.", +"Tue." => "Mar.", +"Wed." => "Mer.", +"Thu." => "Jeu", +"Fri." => "Ven.", +"Sat." => "Sam.", +"January" => "Janvier", +"February" => "Février", +"March" => "Mars", +"April" => "Avril", +"May" => "Mai", +"June" => "Juin", +"July" => "Juillet", +"August" => "Août", +"September" => "Septembre", +"October" => "Octobre", +"November" => "Novembre", +"December" => "Décembre", +"Jan." => "Jan.", +"Feb." => "Fév.", +"Mar." => "Mars", +"Apr." => "Avr.", +"May." => "Mai", +"Jun." => "Juin", +"Jul." => "Juil.", +"Aug." => "Août", +"Sep." => "Sep.", +"Oct." => "Oct.", +"Nov." => "Nov.", +"Dec." => "Déc.", +"All day" => "Journée entière", +"New Calendar" => "Nouveau Calendrier", +"Missing or invalid fields" => "Champs manquants ou invalides", +"Title" => "Titre", +"From Date" => "De la date", +"From Time" => "De l'heure", +"To Date" => "A la date", +"To Time" => "A l'heure", +"The event ends before it starts" => "L'événement s'est terminé avant qu'il ne commence", +"There was a database fail" => "Il y a eu un échec dans la base de donnée", +"Birthday" => "Anniversaire", +"Business" => "Professionnel", +"Call" => "Appel", +"Clients" => "Clientèle", +"Deliverer" => "Livraison", +"Holidays" => "Vacances", +"Ideas" => "Idées", +"Journey" => "Déplacement", +"Jubilee" => "Jubilé", +"Meeting" => "Meeting", +"Other" => "Autre", +"Personal" => "Personnel", +"Projects" => "Projets", +"Questions" => "Questions", +"Work" => "Travail", +"by" => "par", +"unnamed" => "sans nom", +"You do not have the permissions to update this calendar." => "Vous n'avez pas les droits suffisants pour mettre à jour ce calendrier.", +"You do not have the permissions to delete this calendar." => "Vous n'avez pas les droits suffisants pour supprimer ce calendrier.", +"You do not have the permissions to add to this calendar." => "Vous n'avez pas les droits suffisants pour ajouter du contenu à ce calendrier.", +"You do not have the permissions to add events to this calendar." => "Vous n'avez pas les droits suffisants pour ajouter des événements à ce calendrier.", +"You do not have the permissions to delete this event." => "Vous n'avez pas les droits suffisants pour supprimer cet événement.", +"Busy" => "Occupé", +"Public" => "Publique", +"Private" => "Privé", +"Confidential" => "Confidentiel", +"Does not repeat" => "Pas de répétition", +"Daily" => "Quotidien", +"Weekly" => "Hebdomadaire", +"Every Weekday" => "Du lundi au vendredi", +"Bi-Weekly" => "Bi-hebdomadaire", +"Monthly" => "Mensuel", +"Yearly" => "Annuel", +"never" => "jamais", +"by occurrences" => "par occurrences", +"by date" => "par date", +"by monthday" => "par jour du mois", +"by weekday" => "par jour de la semaine", +"events week of month" => "événements du mois par semaine", +"first" => "premier", +"second" => "deuxième", +"third" => "troisième", +"fourth" => "quatrième", +"fifth" => "cinquième", +"last" => "dernier", +"by events date" => "par date d’événements", +"by yearday(s)" => "par jour(s) de l'année", +"by weeknumber(s)" => "par numéro de semaine(s)", +"by day and month" => "par jour et mois", +"Date" => "Date", +"Cal." => "Cal.", +"Week" => "Semaine", +"Month" => "Mois", +"List" => "Liste", +"Today" => "Aujourd'hui", +"Settings" => "Paramètres", +"Your calendars" => "Vos calendriers", +"CalDav Link" => "Lien CalDav", +"Share Calendar" => "Partager le calendrier", +"Download" => "Télécharger", +"Edit" => "Éditer", +"Delete" => "Supprimer", +"New calendar" => "Nouveau calendrier", +"Edit calendar" => "Éditer le calendrier", +"Displayname" => "Nom d'affichage", +"Active" => "Actif", +"Calendar color" => "Couleur du calendrier", +"Save" => "Sauvegarder", +"Submit" => "Soumettre", +"Cancel" => "Annuler", +"Edit an event" => "Éditer un événement", +"Export" => "Exporter", +"Eventinfo" => "Événement", +"Repeating" => "Occurences", +"Alarm" => "Alarmes", +"Attendees" => "Participants", +"Share" => "Partage", +"Title of the Event" => "Titre de l'événement", +"Category" => "Catégorie", +"Separate categories with commas" => "Séparer les catégories par des virgules", +"Edit categories" => "Modifier les catégories", +"Access Class" => "Classe d'accès", +"All Day Event" => "Journée entière", +"From" => "De", +"To" => "À", +"Advanced options" => "Options avancées", +"Location" => "Emplacement", +"Location of the Event" => "Emplacement de l'événement", +"Description" => "Description", +"Description of the Event" => "Description de l'événement", +"Repeat" => "Répétition", +"Advanced" => "Avancé", +"Select weekdays" => "Sélection des jours de la semaine", +"Select days" => "Sélection des jours", +"and the events day of year." => "et les événements de l'année par jour.", +"and the events day of month." => "et les événements du mois par jour.", +"Select months" => "Sélection des mois", +"Select weeks" => "Sélection des semaines", +"and the events week of year." => "et les événements de l'année par semaine.", +"Interval" => "Intervalle", +"End" => "Fin", +"occurrences" => "occurrences", +"create a new calendar" => "Créer un nouveau calendrier", +"Import a calendar file" => "Importer un fichier de calendriers", +"Please choose a calendar" => "Veuillez sélectionner un calendrier", +"Name of new calendar" => "Nom pour le nouveau calendrier", +"Take an available name!" => "Choisissez un nom disponible !", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Un calendrier de ce nom existe déjà. Si vous choisissez de continuer les calendriers seront fusionnés.", +"Remove all events from the selected calendar" => "Supprimer tous les événements du calendrier sélectionné", +"Import" => "Importer", +"Close Dialog" => "Fermer la fenêtre", +"Create a new event" => "Créer un nouvel événement", +"Share with:" => "Partager avec :", +"Shared with" => "Partagé avec", +"Unshare" => "Ne plus partager", +"Nobody" => "Personne", +"Shared via calendar" => "Partagé via le calendrier", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTE : Les actions effectuées sur les événements partagés via calendrier affecteront l'ensemble du partage de calendriers.", +"View an event" => "Voir un événement", +"No categories selected" => "Aucune catégorie sélectionnée", +"of" => "de", +"at" => "à", +"General" => "Généraux", +"Timezone" => "Fuseau horaire", +"Update timezone automatically" => "Mise à jour automatique du fuseau horaire", +"Time format" => "Format de l'heure", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Les semaines commencent le", +"Cache" => "Cache", +"Clear cache for repeating events" => "Nettoyer le cache des événements répétitifs", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Adresses de synchronisation des calendriers CalDAV", +"more info" => "plus d'infos", +"Primary address (Kontact et al)" => "Adresses principales (Kontact et assimilés)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "lien(s) iCalendar en lecture seule" +); diff --git a/l10n/gl.php b/l10n/gl.php new file mode 100644 index 000000000..2920bb82b --- /dev/null +++ b/l10n/gl.php @@ -0,0 +1,215 @@ + "Non todos os calendarios están completamente na caché", +"Everything seems to be completely cached" => "Rematouse e todo está na caché", +"No calendars found." => "Non se atoparon calendarios.", +"No events found." => "Non se atoparon actividades.", +"Wrong calendar" => "Calendario trabucado", +"You do not have the permissions to edit this event." => "Non ten permiso para editar esta actividade.", +"The file contained either no events or all events are already saved in your calendar." => "Ou ben o ficheiro non ten actividades ou todas elas están anotadas xa no seu calendario.", +"events has been saved in the new calendar" => "gardáronse as actividades no novo calendario", +"Import failed" => "Produciuse un fallo na importación", +"events has been saved in your calendar" => "gardáronse as actividades no seu calendario", +"New Timezone:" => "Novo fuso horario:", +"Timezone changed" => "Cambiouse o fuso horario", +"Invalid request" => "Petición incorrecta", +"Calendar" => "Calendario", +"Deletion failed" => "Produciuse un fallo ao eliminar", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "usuario", +"group" => "grupo", +"Editable" => "Editábel", +"Shareable" => "Compartíbel", +"Deletable" => "Eliminábel", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "d MMM[ yyyy]{ '—'d [ MMM] yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d,yyyy", +"Sunday" => "Domingo", +"Monday" => "Luns", +"Tuesday" => "Martes", +"Wednesday" => "Mércores", +"Thursday" => "Xoves", +"Friday" => "Venres", +"Saturday" => "Sábado", +"Sun." => "Dom.", +"Mon." => "Lun.", +"Tue." => "Mar.", +"Wed." => "Mér.", +"Thu." => "Xov.", +"Fri." => "Ven.", +"Sat." => "Sáb.", +"January" => "xaneiro", +"February" => "febreiro", +"March" => "marzo", +"April" => "abril", +"May" => "maio", +"June" => "xuño", +"July" => "xullo", +"August" => "agosto", +"September" => "setembro", +"October" => "outubro", +"November" => "novembro", +"December" => "decembro", +"Jan." => "xan.", +"Feb." => "feb.", +"Mar." => "mar.", +"Apr." => "abr.", +"May." => "mai.", +"Jun." => "xuñ.", +"Jul." => "xul.", +"Aug." => "ago.", +"Sep." => "set.", +"Oct." => "out.", +"Nov." => "nov.", +"Dec." => "dec.", +"All day" => "Todo o dia", +"New Calendar" => "Novo calendario", +"Missing or invalid fields" => "Faltan ou son campos incorrectos", +"Title" => "Título", +"From Date" => "Desde a data", +"From Time" => "Desde a hora", +"To Date" => "Para a data", +"To Time" => "Para a hora", +"The event ends before it starts" => "A actividade remata antes de iniciarse", +"There was a database fail" => "Produciuse un erro na base de datos", +"Birthday" => "Aniversario", +"Business" => "Traballo", +"Call" => "Chamada", +"Clients" => "Clientes", +"Deliverer" => "Remitente", +"Holidays" => "Vacacións", +"Ideas" => "Ideas", +"Journey" => "Viaxe", +"Jubilee" => "Aniversario especial", +"Meeting" => "Xuntanza", +"Other" => "Outro", +"Personal" => "Persoal", +"Projects" => "Proxectos", +"Questions" => "Preguntas", +"Work" => "Traballo", +"by" => "por", +"unnamed" => "sen nome", +"You do not have the permissions to update this calendar." => "Non ten permiso para actualizar este calendario.", +"You do not have the permissions to delete this calendar." => "Non ten permiso para eliminar este calendario.", +"You do not have the permissions to add to this calendar." => "Non ten permiso para engadir este calendario.", +"You do not have the permissions to add events to this calendar." => "Non ten permiso para engadir novas actividades neste calendario.", +"You do not have the permissions to delete this event." => "Non ten permiso para eliminar esta actividade.", +"Busy" => "Ocupado", +"Public" => "Público", +"Private" => "Privado", +"Confidential" => "Confidencial", +"Does not repeat" => "Non se repite", +"Daily" => "Diariamente", +"Weekly" => "Semanalmente", +"Every Weekday" => "Todas as semanas", +"Bi-Weekly" => "Cada dúas semanas", +"Monthly" => "Mensual", +"Yearly" => "Anual", +"never" => "nunca", +"by occurrences" => "por acontecementos", +"by date" => "por data", +"by monthday" => "por día do mes", +"by weekday" => "por día da semana", +"events week of month" => "actividades semanais do mes", +"first" => "primeiro", +"second" => "segundo", +"third" => "terceiro", +"fourth" => "cuarto", +"fifth" => "quinto", +"last" => "último", +"by events date" => "por data das actividades", +"by yearday(s)" => "por dia(s) do ano", +"by weeknumber(s)" => "por número(s) de semana", +"by day and month" => "por día e mes", +"Date" => "Data", +"Cal." => "Cal.", +"Week" => "Semana", +"Month" => "Mes", +"List" => "Lista", +"Today" => "Hoxe", +"Settings" => "Configuracións", +"Your calendars" => "Os seus calendarios", +"CalDav Link" => "Ligazón CalDav", +"Share Calendar" => "Compartir o calendario", +"Download" => "Descargar", +"Edit" => "Editar", +"Delete" => "Eliminar", +"New calendar" => "Novo calendario", +"Edit calendar" => "Editar o calendario", +"Displayname" => "Amosar o nome", +"Active" => "Activo", +"Calendar color" => "Cor do calendario", +"Save" => "Gardar", +"Submit" => "Enviar", +"Cancel" => "Cancelar", +"Edit an event" => "Editar unha actividade", +"Export" => "Exportar", +"Eventinfo" => "Info. da actividade", +"Repeating" => "Repetido", +"Alarm" => "Alarma", +"Attendees" => "Participantes", +"Share" => "Compartir", +"Title of the Event" => "Título da actividade", +"Category" => "Categoría", +"Separate categories with commas" => "Separe as categorías con comas", +"Edit categories" => "Editar as categorías", +"Access Class" => "Clase de acceso", +"All Day Event" => "Actividades de todo o día", +"From" => "Desde", +"To" => "a", +"Advanced options" => "Opcións avanzadas", +"Location" => "Lugar", +"Location of the Event" => "Lugar da actividade", +"Description" => "Descrición", +"Description of the Event" => "Descrición da actividade", +"Repeat" => "Repetir", +"Advanced" => "Avanzado", +"Select weekdays" => "Seleccionar días da semana", +"Select days" => "Seleccionar días", +"and the events day of year." => "e as actividades diarias do ano.", +"and the events day of month." => "e as actividades diarias do mes.", +"Select months" => "Seleccionar meses", +"Select weeks" => "Seleccionar semanas", +"and the events week of year." => "e as actividades semanais do ano.", +"Interval" => "Intervalo", +"End" => "Fin", +"occurrences" => "acontecementos", +"create a new calendar" => "crear un novo calendario", +"Import a calendar file" => "Importar un ficheiro de calendario", +"Please choose a calendar" => "Escolla un calendario", +"Name of new calendar" => "Nome do novo calendario", +"Take an available name!" => "Escolla un nome dispoñíbel", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Xa hai un calendario con ese nome. Sen lle cambiar o nome xuntaranse os dous calendarios.", +"Remove all events from the selected calendar" => "Eliminar todas as actividades do calendario escollido.", +"Import" => "Importar", +"Close Dialog" => "Pechar o diálogo", +"Create a new event" => "Crear unha nova actividade", +"Share with:" => "Compartir con:", +"Shared with" => "Compartido con", +"Unshare" => "Deixar de compartir", +"Nobody" => "Ninguén", +"Shared via calendar" => "Compartir por calendario", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTA: As accións nas actividades compartidas por calendario afectan a todo o compartido polo calendario.", +"View an event" => "Ver unha actividade", +"No categories selected" => "Non foi seleccionada ningunha categoría", +"of" => "de", +"at" => "a", +"General" => "Xeral", +"Timezone" => "Fuso horario", +"Update timezone automatically" => "Actualizar o fuso horario automaticamente", +"Time format" => "Formato da hora", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Comezar a semana o ", +"Cache" => "Caché", +"Clear cache for repeating events" => "Limpar a caché para as actividades que se repiten", +"URLs" => "URL", +"Calendar CalDAV syncing addresses" => "Enderezos de sincronización do calendario CalDAV", +"more info" => "máis información", +"Primary address (Kontact et al)" => "Enderezo primario (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Ligazón(s) ao(s) iCalendarios de só lectura" +); diff --git a/l10n/he.php b/l10n/he.php new file mode 100644 index 000000000..53e2d7c8f --- /dev/null +++ b/l10n/he.php @@ -0,0 +1,215 @@ + "לא כל לוחות השנה נשמרו בזיכרון המטמון", +"Everything seems to be completely cached" => "נראה שכל המידע נשמר בזיכרון המטמון", +"No calendars found." => "לא נמצאו לוחות שנה.", +"No events found." => "לא נמצאו אירועים.", +"Wrong calendar" => "לוח שנה לא נכון", +"You do not have the permissions to edit this event." => "אין לך הרשאות לערוך את האירוע הזה.", +"The file contained either no events or all events are already saved in your calendar." => "או שהקובץ לא מכיל אירועים או שכל האירועים כבר נשמרו בלוח השנה", +"events has been saved in the new calendar" => "האירועים נשמרו בלוח השנה החדש", +"Import failed" => "כשלון ביבוא", +"events has been saved in your calendar" => "אירועים נשמרו בלוח השנה שלך", +"New Timezone:" => "אזור זמן חדש:", +"Timezone changed" => "אזור זמן השתנה", +"Invalid request" => "בקשה לא חוקית", +"Calendar" => "לוח שנה", +"Deletion failed" => "מחיקה נכשלה", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "משתמש", +"group" => "קבוצה", +"Editable" => "ניתן לעריכה", +"Shareable" => "ניתן לשיתוף", +"Deletable" => "ניתן למחיקה", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "d MMM [ yyyy]{ '—'d[ MMM] yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "יום ראשון", +"Monday" => "יום שני", +"Tuesday" => "יום שלישי", +"Wednesday" => "יום רביעי", +"Thursday" => "יום חמישי", +"Friday" => "יום שישי", +"Saturday" => "שבת", +"Sun." => "א", +"Mon." => "ב", +"Tue." => "ג", +"Wed." => "ד", +"Thu." => "ה", +"Fri." => "ו", +"Sat." => "ש", +"January" => "ינואר", +"February" => "פברואר", +"March" => "מרץ", +"April" => "אפריל", +"May" => "מאי", +"June" => "יוני", +"July" => "יולי", +"August" => "אוגוסט", +"September" => "ספטמבר", +"October" => "אוקטובר", +"November" => "נובמבר", +"December" => "דצמבר", +"Jan." => "ינו׳", +"Feb." => "פבר׳", +"Mar." => "מרץ", +"Apr." => "אפר׳", +"May." => "מאי", +"Jun." => "יונ׳", +"Jul." => "יול׳", +"Aug." => "אוג׳", +"Sep." => "ספט׳", +"Oct." => "אוק׳", +"Nov." => "נוב׳", +"Dec." => "דצמ׳", +"All day" => "היום", +"New Calendar" => "לוח שנה חדש", +"Missing or invalid fields" => "שדות שגויים או חסרים", +"Title" => "כותרת", +"From Date" => "מתאריך", +"From Time" => "משעה", +"To Date" => "עד תאריך", +"To Time" => "עד שעה", +"The event ends before it starts" => "האירוע מסתיים עוד לפני שהתחיל", +"There was a database fail" => "אירע כשל במסד הנתונים", +"Birthday" => "יום הולדת", +"Business" => "עסקים", +"Call" => "שיחה", +"Clients" => "לקוחות", +"Deliverer" => "משלוח", +"Holidays" => "חגים", +"Ideas" => "רעיונות", +"Journey" => "מסע", +"Jubilee" => "יובל", +"Meeting" => "פגישה", +"Other" => "אחר", +"Personal" => "אישי", +"Projects" => "פרוייקטים", +"Questions" => "שאלות", +"Work" => "עבודה", +"by" => "מאת", +"unnamed" => "ללא שם", +"You do not have the permissions to update this calendar." => "אין לך הרשאות לעדכן את לוח השנה הזה.", +"You do not have the permissions to delete this calendar." => "אין לך הרשאות למחוק את לוח השנה הזה.", +"You do not have the permissions to add to this calendar." => "אין לך הרשאות להוסיף את לוח השנה הזה.", +"You do not have the permissions to add events to this calendar." => "אין לך הרשאות להוספת אירועים ללוח השנה הזה.", +"You do not have the permissions to delete this event." => "אין לך הרשאות למחוק את האירוע הזה.", +"Busy" => "עסוק", +"Public" => "ציבורי", +"Private" => "פרטי", +"Confidential" => "חסוי", +"Does not repeat" => "ללא חזרה", +"Daily" => "יומי", +"Weekly" => "שבועי", +"Every Weekday" => "כל יום עבודה", +"Bi-Weekly" => "דו שבועי", +"Monthly" => "חודשי", +"Yearly" => "שנתי", +"never" => "לעולם לא", +"by occurrences" => "לפי מופעים", +"by date" => "לפי תאריך", +"by monthday" => "לפי היום בחודש", +"by weekday" => "לפי היום בשבוע", +"events week of month" => "שבוע בחודש לציון הפעילות", +"first" => "ראשון", +"second" => "שני", +"third" => "שלישי", +"fourth" => "רביעי", +"fifth" => "חמישי", +"last" => "אחרון", +"by events date" => "לפי תאריכי האירועים", +"by yearday(s)" => "לפי ימים בשנה", +"by weeknumber(s)" => "לפי מספרי השבועות", +"by day and month" => "לפי יום וחודש", +"Date" => "תאריך", +"Cal." => "לוח שנה", +"Week" => "שבוע", +"Month" => "חודש", +"List" => "רשימה", +"Today" => "היום", +"Settings" => "הגדרות", +"Your calendars" => "לוחות השנה שלך", +"CalDav Link" => "קישור CalDav", +"Share Calendar" => "שיתוף לוח שנה", +"Download" => "הורדה", +"Edit" => "עריכה", +"Delete" => "מחיקה", +"New calendar" => "לוח שנה חדש", +"Edit calendar" => "עריכת לוח שנה", +"Displayname" => "שם תצוגה", +"Active" => "פעיל", +"Calendar color" => "צבע לוח שנה", +"Save" => "שמירה", +"Submit" => "שליחה", +"Cancel" => "ביטול", +"Edit an event" => "עריכת אירוע", +"Export" => "יצוא", +"Eventinfo" => "פרטי האירוע", +"Repeating" => "חוזר", +"Alarm" => "תזכורת", +"Attendees" => "משתתפים", +"Share" => "שיתוף", +"Title of the Event" => "כותרת האירוע", +"Category" => "קטגוריה", +"Separate categories with commas" => "הפרדת קטגוריות בפסיק", +"Edit categories" => "עריכת קטגוריות", +"Access Class" => "סיווג גישה", +"All Day Event" => "אירוע של כל היום", +"From" => "מאת", +"To" => "עבור", +"Advanced options" => "אפשרויות מתקדמות", +"Location" => "מיקום", +"Location of the Event" => "מיקום האירוע", +"Description" => "תיאור", +"Description of the Event" => "תיאור האירוע", +"Repeat" => "חזרה", +"Advanced" => "מתקדם", +"Select weekdays" => "יש לבחור ימים בשבוע", +"Select days" => "יש לבחור בימים", +"and the events day of year." => "ויום האירוע בשנה.", +"and the events day of month." => "ויום האירוע בחודש.", +"Select months" => "יש לבחור בחודשים", +"Select weeks" => "יש לבחור בשבועות", +"and the events week of year." => "ומספור השבוע הפעיל בשנה.", +"Interval" => "משך", +"End" => "סיום", +"occurrences" => "מופעים", +"create a new calendar" => "יצירת לוח שנה חדש", +"Import a calendar file" => "יבוא קובץ לוח שנה", +"Please choose a calendar" => "נא לבחור לוח שנה", +"Name of new calendar" => "שם לוח השנה החדש", +"Take an available name!" => "בחר שם פנוי!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "לוח שנה בשם הזה כבר קיים. אם תמשיך בכל זאת, לוחות השנה ימוזגו.", +"Remove all events from the selected calendar" => "הסרת כל האירועים מהיומן הנבחר", +"Import" => "יבוא", +"Close Dialog" => "סגירת הדו־שיח", +"Create a new event" => "יצירת אירוע חדש", +"Share with:" => "שתף עם:", +"Shared with" => "משותף עם:", +"Unshare" => "הסר שיתוף", +"Nobody" => "אף אחד", +"Shared via calendar" => "משותף דרך לוח שנה", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "הערה: פעולות על אירועים שמשותפים דרך לוח השנה ישפיעו על שיתוף לוח השנה כולו.", +"View an event" => "צפייה באירוע", +"No categories selected" => "לא נבחרו קטגוריות", +"of" => "מתוך", +"at" => "בשנה", +"General" => "כללי", +"Timezone" => "אזור זמן", +"Update timezone automatically" => "עדכון אזור זמן אוטומטי", +"Time format" => "תבנית זמן", +"24h" => "24 שעות", +"12h" => "12 שעות", +"Start week on" => "שבוע מתחיל ביום", +"Cache" => "מטמון", +"Clear cache for repeating events" => "נקה מטמון עבור אירועים חוזרים", +"URLs" => "כתובות", +"Calendar CalDAV syncing addresses" => "כתובות סנכרון ללוח שנה CalDAV", +"more info" => "מידע נוסף", +"Primary address (Kontact et al)" => "כתובת ראשית (קונטקט ואחרים)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "קרא רק קישורי iCalendar" +); diff --git a/l10n/hi.php b/l10n/hi.php new file mode 100644 index 000000000..fef8d3dab --- /dev/null +++ b/l10n/hi.php @@ -0,0 +1,4 @@ + "यक्तिगत", +"Settings" => "सेटिंग्स" +); diff --git a/l10n/hr.php b/l10n/hr.php new file mode 100644 index 000000000..ddc99cdee --- /dev/null +++ b/l10n/hr.php @@ -0,0 +1,143 @@ + "Nisu pronađeni kalendari", +"No events found." => "Događaj nije pronađen.", +"Wrong calendar" => "Pogrešan kalendar", +"New Timezone:" => "Nova vremenska zona:", +"Timezone changed" => "Vremenska zona promijenjena", +"Invalid request" => "Neispravan zahtjev", +"Calendar" => "Kalendar", +"Editable" => "Može se uređivati", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"Sunday" => "nedelja", +"Monday" => "ponedeljak", +"Tuesday" => "utorak", +"Wednesday" => "srijeda", +"Thursday" => "četvrtak", +"Friday" => "petak", +"Saturday" => "subota", +"January" => "siječanj", +"February" => "veljača", +"March" => "ožujak", +"April" => "travanj", +"May" => "svibanj", +"June" => "lipanj", +"July" => "srpanj", +"August" => "kolovoz", +"September" => "rujan", +"October" => "listopad", +"November" => "studeni", +"December" => "prosinac", +"All day" => "Cijeli dan", +"New Calendar" => "Novi kalendar", +"Title" => "Naslov", +"From Date" => "Datum od", +"From Time" => "Vrijeme od", +"To Date" => "Datum do", +"To Time" => "Vrijeme do", +"The event ends before it starts" => "Događaj završava prije nego počinje", +"There was a database fail" => "Pogreška u bazi podataka", +"Birthday" => "Rođendan", +"Business" => "Poslovno", +"Call" => "Poziv", +"Clients" => "Klijenti", +"Deliverer" => "Dostavljač", +"Holidays" => "Praznici", +"Ideas" => "Ideje", +"Journey" => "Putovanje", +"Jubilee" => "Obljetnica", +"Meeting" => "Sastanak", +"Other" => "Ostalo", +"Personal" => "Osobno", +"Projects" => "Projekti", +"Questions" => "Pitanja", +"Work" => "Posao", +"unnamed" => "bezimeno", +"Does not repeat" => "Ne ponavlja se", +"Daily" => "Dnevno", +"Weekly" => "Tjedno", +"Every Weekday" => "Svakog radnog dana", +"Bi-Weekly" => "Dvotjedno", +"Monthly" => "Mjesečno", +"Yearly" => "Godišnje", +"never" => "nikad", +"by occurrences" => "po pojavama", +"by date" => "po datum", +"by monthday" => "po dana mjeseca", +"by weekday" => "po tjednu", +"first" => "prvi", +"second" => "drugi", +"third" => "treći", +"fourth" => "četvrti", +"fifth" => "peti", +"last" => "zadnji", +"by events date" => "po datumu događaja", +"by yearday(s)" => "po godini(-nama)", +"by weeknumber(s)" => "po broju tjedna(-ana)", +"by day and month" => "po danu i mjeseca", +"Date" => "datum", +"Cal." => "Kal.", +"Week" => "Tjedan", +"Month" => "Mjesec", +"List" => "Lista", +"Today" => "Danas", +"Settings" => "Postavke", +"Your calendars" => "Vaši kalendari", +"CalDav Link" => "CalDav poveznica", +"Share Calendar" => "Podjeli kalendar", +"Download" => "Spremi lokalno", +"Edit" => "Uredi", +"Delete" => "Briši", +"New calendar" => "Novi kalendar", +"Edit calendar" => "Uredi kalendar", +"Displayname" => "Naziv", +"Active" => "Aktivan", +"Calendar color" => "Boja kalendara", +"Save" => "Spremi", +"Submit" => "Potvrdi", +"Cancel" => "Odustani", +"Edit an event" => "Uredi događaj", +"Export" => "Izvoz", +"Eventinfo" => "Informacije o događaju", +"Repeating" => "Ponavljanje", +"Alarm" => "Alarm", +"Attendees" => "Polaznici", +"Share" => "Podijeli", +"Title of the Event" => "Naslov događaja", +"Category" => "Kategorija", +"Separate categories with commas" => "Odvoji kategorije zarezima", +"Edit categories" => "Uredi kategorije", +"All Day Event" => "Cjelodnevni događaj", +"From" => "Od", +"To" => "Za", +"Advanced options" => "Napredne mogućnosti", +"Location" => "Lokacija", +"Location of the Event" => "Lokacija događaja", +"Description" => "Opis", +"Description of the Event" => "Opis događaja", +"Repeat" => "Ponavljanje", +"Advanced" => "Napredno", +"Select weekdays" => "Odaberi dane u tjednu", +"Select days" => "Odaberi dane", +"Select months" => "Odaberi mjesece", +"Select weeks" => "Odaberi tjedne", +"Interval" => "Interval", +"End" => "Kraj", +"occurrences" => "pojave", +"create a new calendar" => "stvori novi kalendar", +"Import a calendar file" => "Uvozite datoteku kalendara", +"Name of new calendar" => "Ime novog kalendara", +"Import" => "Uvoz", +"Close Dialog" => "Zatvori dijalog", +"Create a new event" => "Unesi novi događaj", +"Unshare" => "Makni djeljenje", +"View an event" => "Vidjeti događaj", +"No categories selected" => "Nema odabranih kategorija", +"of" => "od", +"at" => "na", +"General" => "Općenito", +"Timezone" => "Vremenska zona", +"24h" => "24h", +"12h" => "12h", +"more info" => "više informacija", +"iOS/OS X" => "iOS/OS X" +); diff --git a/l10n/hu_HU.php b/l10n/hu_HU.php new file mode 100644 index 000000000..325ee96e2 --- /dev/null +++ b/l10n/hu_HU.php @@ -0,0 +1,215 @@ + "Nem minden naptár van a gyorsítótárban", +"Everything seems to be completely cached" => "Úgy tűnik, hogy minden gyorsítótárazva van", +"No calendars found." => "Nem található naptár", +"No events found." => "Nem található esemény", +"Wrong calendar" => "Hibás naptár", +"You do not have the permissions to edit this event." => "Nincs joga ahhoz, hogy módosítsa ezt az eseményt.", +"The file contained either no events or all events are already saved in your calendar." => "A fájl nem tartalmazott eseményeket, vagy pedig az események már el vannak mentve a naptárban.", +"events has been saved in the new calendar" => "események mentve az új naptárba", +"Import failed" => "Az importálás nem sikerült", +"events has been saved in your calendar" => "esemény került rögzítésre a naptárban", +"New Timezone:" => "Új időzóna:", +"Timezone changed" => "Az időzóna megváltozott", +"Invalid request" => "Érvénytelen kérés", +"Calendar" => "Naptár", +"Deletion failed" => "A törlés nem sikerült", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "felhasználó", +"group" => "csoport", +"Editable" => "Szerkeszthető", +"Shareable" => "Megosztható", +"Deletable" => "Törölhető", +"ddd" => "ddd", +"ddd M/d" => "ddd M.d.", +"dddd M/d" => "dddd M.d.", +"MMMM yyyy" => "yyyy MMMM", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "vasárnap", +"Monday" => "hétfő", +"Tuesday" => "kedd", +"Wednesday" => "szerda", +"Thursday" => "csütörtök", +"Friday" => "péntek", +"Saturday" => "szombat", +"Sun." => "V.", +"Mon." => "H.", +"Tue." => "K.", +"Wed." => "Sze.", +"Thu." => "Cs.", +"Fri." => "P.", +"Sat." => "Szo.", +"January" => "január", +"February" => "február", +"March" => "március", +"April" => "április", +"May" => "május", +"June" => "június", +"July" => "július", +"August" => "augusztus", +"September" => "szeptember", +"October" => "október", +"November" => "november", +"December" => "december", +"Jan." => "jan.", +"Feb." => "feb.", +"Mar." => "márc.", +"Apr." => "ápr.", +"May." => "máj.", +"Jun." => "jún.", +"Jul." => "júl.", +"Aug." => "aug.", +"Sep." => "szept.", +"Oct." => "okt.", +"Nov." => "nov.", +"Dec." => "dec.", +"All day" => "Egész nap", +"New Calendar" => "Új naptár", +"Missing or invalid fields" => "Kitöltetlen vagy érvénytelenül kitöltött mezők", +"Title" => "Cím", +"From Date" => "Napjától", +"From Time" => "Időtől", +"To Date" => "Napig", +"To Time" => "Ideig", +"The event ends before it starts" => "Az esemény a kezdete előtt ér véget.", +"There was a database fail" => "Adatbázis hiba történt", +"Birthday" => "Születésap", +"Business" => "Üzleti", +"Call" => "Hívás", +"Clients" => "Kliensek", +"Deliverer" => "Szállító", +"Holidays" => "Ünnepek", +"Ideas" => "Ötletek", +"Journey" => "Utazás", +"Jubilee" => "Évforduló", +"Meeting" => "Találkozó", +"Other" => "Egyéb", +"Personal" => "Személyes", +"Projects" => "Projektek", +"Questions" => "Kérdések", +"Work" => "Munka", +"by" => "tőle", +"unnamed" => "névtelen", +"You do not have the permissions to update this calendar." => "Nincs joga ahhoz, hogy frissítse ezt a naptárat.", +"You do not have the permissions to delete this calendar." => "Nincs joga ahhoz, hogy törölje ezt a naptárat.", +"You do not have the permissions to add to this calendar." => "Nincs joga ahhoz, hogy hozzáadjon ehhez a naptárhoz.", +"You do not have the permissions to add events to this calendar." => "Nincs joga ahhoz, hogy eseményeket illesszen ebbe a naptárba.", +"You do not have the permissions to delete this event." => "Nincs joga ahhoz, hogy törölje ezt az eseményt.", +"Busy" => "Elfoglalt", +"Public" => "Publikus", +"Private" => "Személyes", +"Confidential" => "Bizalmas", +"Does not repeat" => "Nem ismétlődik", +"Daily" => "Naponta", +"Weekly" => "Hetente", +"Every Weekday" => "Minden hétköznap", +"Bi-Weekly" => "Kéthetente", +"Monthly" => "Havonta", +"Yearly" => "Évente", +"never" => "soha", +"by occurrences" => "előfordulások száma szerint", +"by date" => "dátum szerint", +"by monthday" => "hónap napja szerint", +"by weekday" => "hét napja szerint", +"events week of month" => "hónap heteinek sorszáma", +"first" => "első", +"second" => "második", +"third" => "harmadik", +"fourth" => "negyedik", +"fifth" => "ötödik", +"last" => "utolsó", +"by events date" => "az esemény napja szerint", +"by yearday(s)" => "az év napja(i) szerint", +"by weeknumber(s)" => "a hét sorszáma szerint", +"by day and month" => "nap és hónap szerint", +"Date" => "Dátum", +"Cal." => "Naptár", +"Week" => "Hét", +"Month" => "Hónap", +"List" => "Lista", +"Today" => "Ma", +"Settings" => "Beállítások", +"Your calendars" => "Az Ön naptárai", +"CalDav Link" => "CalDAV link", +"Share Calendar" => "A naptár megosztása", +"Download" => "Letöltés", +"Edit" => "Szerkesztés", +"Delete" => "Törlés", +"New calendar" => "Új naptár", +"Edit calendar" => " A naptár szerkesztése", +"Displayname" => "Megjelenítési név", +"Active" => "Aktív", +"Calendar color" => "A naptár színe", +"Save" => "Mentés", +"Submit" => "Beküldés", +"Cancel" => "Mégse", +"Edit an event" => "Esemény szerkesztése", +"Export" => "Export", +"Eventinfo" => "Eseményinfó", +"Repeating" => "Ismétlődő", +"Alarm" => "Riasztás", +"Attendees" => "Résztvevők", +"Share" => "Megosztás", +"Title of the Event" => "Az esemény címe", +"Category" => "Kategória", +"Separate categories with commas" => "Vesszővel válassza el a kategóriákat", +"Edit categories" => "Kategóriák szerkesztése", +"Access Class" => "Hozzáférési osztály", +"All Day Event" => "Egész napos esemény", +"From" => "Ettől", +"To" => "Eddig", +"Advanced options" => "Haladó beállítások", +"Location" => "Hely", +"Location of the Event" => "Az esemény helyszíne", +"Description" => "Leírás", +"Description of the Event" => "Az esemény leírása", +"Repeat" => "Ismétlés", +"Advanced" => "Haladó", +"Select weekdays" => "Hétköznapok kiválasztása", +"Select days" => "Napok kiválasztása", +"and the events day of year." => "és az éves esemény napja.", +"and the events day of month." => "és a havi esemény napja.", +"Select months" => "Hónapok kiválasztása", +"Select weeks" => "Hetek kiválasztása", +"and the events week of year." => "és az heti esemény napja.", +"Interval" => "Időköz", +"End" => "Vége", +"occurrences" => "előfordulás", +"create a new calendar" => "új naptár létrehozása", +"Import a calendar file" => "Naptárállomány importálása", +"Please choose a calendar" => "Kérem válasszon egy naptárt", +"Name of new calendar" => "Az új naptár neve", +"Take an available name!" => "Válasszon egy rendelkezésre álló nevet!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Egy naptár létezik már ezen a néven. Ha folytatja, akkor a naptárak összevonásra kerülnek.", +"Remove all events from the selected calendar" => "Összes esemény eltávolítása a kiválasztott naptárból", +"Import" => "Importálás", +"Close Dialog" => "A párbeszédablak bezárása", +"Create a new event" => "Új esemény létrehozása", +"Share with:" => "Megosztás:", +"Shared with" => "Meg van osztva a következőkkel:", +"Unshare" => "A megosztás visszavonása", +"Nobody" => "Senki", +"Shared via calendar" => "Az egész naptár megosztása miatt meg van osztva a következőkkel:", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "Figyelem: a naptárokban megosztott események módosítása a megosztott információt is módosítja.", +"View an event" => "Esemény megtekintése", +"No categories selected" => "Nincs kiválasztott kategória", +"of" => ", tulajdonos", +"at" => ", ", +"General" => "Általános", +"Timezone" => "Időzóna", +"Update timezone automatically" => "Az időzóna automatikus frissítése", +"Time format" => "Az idő formátuma", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "A hét első napja", +"Cache" => "Gyorsítótár", +"Clear cache for repeating events" => "Az ismétlődő eseményeknél a gyorsítótár törlése", +"URLs" => "URL-ek", +"Calendar CalDAV syncing addresses" => "A naptár CaIDAV szinkronizálási címe", +"more info" => "további információ", +"Primary address (Kontact et al)" => "Elsődleges cím (Kontact és már programok)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "iCalendar cím (csak olvasásra)" +); diff --git a/l10n/hy.php b/l10n/hy.php new file mode 100644 index 000000000..93486fa0f --- /dev/null +++ b/l10n/hy.php @@ -0,0 +1,37 @@ + "Օրացույց", +"Sunday" => "Կիրակի", +"Monday" => "Երկուշաբթի", +"Tuesday" => "Երեքշաբթի", +"Wednesday" => "Չորեքշաբթի", +"Thursday" => "Հինգշաբթի", +"Friday" => "Ուրբաթ", +"Saturday" => "Շաբաթ", +"Sun." => "Կիր.", +"Mon." => "Երկ.", +"Tue." => "Երք.", +"Wed." => "Չոր.", +"Thu." => "Հնգ.", +"Fri." => "Ուրբ.", +"Sat." => "Շաբ.", +"January" => "Հունվար", +"February" => "Փետրվար", +"March" => "Մարտ", +"April" => "Ապրիլ", +"May" => "Մայիս", +"June" => "Հունիս", +"July" => "Հուլիս", +"August" => "Օգոստոս", +"September" => "Սեպտեմբեր", +"October" => "Հոկտեմբեր", +"November" => "Նոյեմբեր", +"December" => "Դեկտեմբեր", +"Other" => "Այլ", +"Month" => "Ամիս", +"Today" => "Այսօր", +"Download" => "Բեռնել", +"Delete" => "Ջնջել", +"Save" => "Պահպանել", +"Submit" => "Հաստատել", +"Description" => "Նկարագրություն" +); diff --git a/l10n/ia.php b/l10n/ia.php new file mode 100644 index 000000000..c77c626d1 --- /dev/null +++ b/l10n/ia.php @@ -0,0 +1,126 @@ + "Necun calendarios trovate.", +"No events found." => "Nulle eventos trovate.", +"New Timezone:" => "Nove fuso horari", +"Invalid request" => "Requesta invalide.", +"Calendar" => "Calendario", +"Sunday" => "Dominica", +"Monday" => "Lunedi", +"Tuesday" => "Martedi", +"Wednesday" => "Mercuridi", +"Thursday" => "Jovedi", +"Friday" => "Venerdi", +"Saturday" => "Sabbato", +"Sun." => "Dom.", +"Mon." => "Lun.", +"Tue." => "Mar.", +"Wed." => "Mer.", +"Thu." => "Jov.", +"Fri." => "Ven.", +"Sat." => "Sab.", +"January" => "januario", +"February" => "Februario", +"March" => "Martio", +"April" => "April", +"May" => "Mai", +"June" => "Junio", +"July" => "Julio", +"August" => "Augusto", +"September" => "Septembre", +"October" => "Octobre", +"November" => "Novembre", +"December" => "Decembre", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Apr.", +"May." => "Mai.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Dec." => "Dec.", +"All day" => "Omne die", +"New Calendar" => "Nove calendario", +"Title" => "Titulo", +"From Date" => "Data de initio", +"From Time" => "Hora de initio", +"To Date" => "Data de fin", +"To Time" => "Hora de fin", +"Birthday" => "Anniversario de nativitate", +"Business" => "Affaires", +"Call" => "Appello", +"Clients" => "Clientes", +"Holidays" => "Dies feriate", +"Meeting" => "Incontro", +"Other" => "Altere", +"Personal" => "Personal", +"Projects" => "Projectos", +"Questions" => "Demandas", +"Work" => "Travalio", +"unnamed" => "sin nomine", +"Does not repeat" => "Non repite", +"Daily" => "Quotidian", +"Weekly" => "Septimanal", +"Every Weekday" => "Cata die", +"Monthly" => "Mensual", +"Yearly" => "Cata anno", +"never" => "nunquam", +"by date" => "per data", +"first" => "prime", +"second" => "secunde", +"third" => "tertie", +"last" => "ultime", +"by events date" => "per data de eventos", +"by day and month" => "per dia e mense", +"Date" => "Data", +"Week" => "Septimana", +"Month" => "Mense", +"List" => "Lista", +"Today" => "Hodie", +"Settings" => "Configurationes", +"Your calendars" => "Tu calendarios", +"Download" => "Discarga", +"Edit" => "Modificar", +"Delete" => "Deler", +"New calendar" => "Nove calendario", +"Edit calendar" => "Modificar calendario", +"Active" => "Active", +"Calendar color" => "Color de calendario", +"Save" => "Salveguardar", +"Submit" => "Inviar", +"Cancel" => "Cancellar", +"Edit an event" => "Modificar evento", +"Export" => "Exportar", +"Share" => "Compartir", +"Title of the Event" => "Titulo del evento.", +"Category" => "Categoria", +"Edit categories" => "Modificar categorias", +"From" => "Ab", +"To" => "A", +"Advanced options" => "Optiones avantiate", +"Location" => "Loco", +"Location of the Event" => "Loco del evento.", +"Description" => "Description", +"Description of the Event" => "Description del evento", +"Repeat" => "Repeter", +"Advanced" => "Avantiate", +"Select weekdays" => "Seliger dies del septimana", +"Select days" => "Seliger dies", +"Select months" => "Seliger menses", +"Select weeks" => "Seliger septimanas", +"Interval" => "Intervallo", +"End" => "Fin", +"create a new calendar" => "crear un nove calendario", +"Import a calendar file" => "Importar un file de calendario", +"Name of new calendar" => "Nomine del calendario", +"Import" => "Importar", +"Close Dialog" => "Clauder dialogo", +"Create a new event" => "Crear un nove evento", +"View an event" => "Vide un evento", +"No categories selected" => "Nulle categorias seligite", +"of" => "de", +"at" => "in", +"Timezone" => "Fuso horari", +"more info" => "plus info", +"iOS/OS X" => "iOS/OS X" +); diff --git a/l10n/id.php b/l10n/id.php new file mode 100644 index 000000000..dae3552d4 --- /dev/null +++ b/l10n/id.php @@ -0,0 +1,182 @@ + "tidak ada kejadian ditemukan", +"Wrong calendar" => "Kalender salah", +"You do not have the permissions to edit this event." => "anda tidak memiliki ijin untuk mengubah kejadian ini", +"Import failed" => "impor gagal", +"New Timezone:" => "zona waktu baru", +"Timezone changed" => "Zona waktu telah diubah", +"Invalid request" => "Permintaan tidak sah", +"Calendar" => "Kalender", +"Deletion failed" => "penghapusan gagal", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "pengguna", +"group" => "grup", +"Editable" => "dapat di ubah", +"Shareable" => "dapat dibagikan", +"Deletable" => "dapat dihapus", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "minggu", +"Monday" => "senin", +"Tuesday" => "selasa", +"Wednesday" => "rabu", +"Thursday" => "kamis", +"Friday" => "jumat", +"Saturday" => "sabtu", +"Sun." => "ming.", +"Mon." => "sen.", +"Tue." => "sel.", +"Wed." => "rab.", +"Thu." => "kam.", +"Fri." => "jum.", +"Sat." => "sab.", +"January" => "januari", +"February" => "februari", +"March" => "maret", +"April" => "april", +"May" => "mei", +"June" => "juni", +"July" => "juli", +"August" => "agustus", +"September" => "september", +"October" => "oktober", +"November" => "november", +"December" => "desember", +"Jan." => "jan.", +"Feb." => "feb.", +"Mar." => "mar.", +"Apr." => "apr.", +"May." => "mei", +"Jun." => "jun.", +"Jul." => "jul.", +"Aug." => "agst.", +"Sep." => "sept.", +"Oct." => "okt.", +"Nov." => "nov.", +"Dec." => "des.", +"All day" => "Semua Hari", +"New Calendar" => "kalender baru", +"Title" => "Judul", +"From Date" => "dari tanggal", +"From Time" => "dari waktu", +"To Date" => "sampai tanggal", +"To Time" => "sampai waktu", +"The event ends before it starts" => "kejadian berakhir sebelum dimulai", +"There was a database fail" => "terjadi kesalahan basis data", +"Birthday" => "Ulang tahun", +"Business" => "Usaha", +"Call" => "panggil", +"Clients" => "Klien", +"Deliverer" => "Pengirim", +"Holidays" => "Hari libur", +"Ideas" => "ide", +"Journey" => "Perjalanan", +"Jubilee" => "peringatan", +"Meeting" => "Rapat", +"Other" => "Lainnya", +"Personal" => "pribadi", +"Projects" => "proyek", +"Questions" => "Pertanyaan", +"Work" => "kerja", +"by" => "oleh", +"unnamed" => "tidak diberi nama", +"You do not have the permissions to update this calendar." => "anda tidak memiliki ijin untuk memperbaharui kalender ini", +"You do not have the permissions to delete this calendar." => "anda tidak memiliki ijin untuk menghapus kalender ini", +"You do not have the permissions to add to this calendar." => "anda tidak memiliki ijin untuk menambahkan ke kalender ini", +"You do not have the permissions to add events to this calendar." => "anda tidak memiliki ijin untuk menambahkan kejadian ke kalender ini", +"You do not have the permissions to delete this event." => "anda tidak memiliki ijin untuk menghapus kejadian ini", +"Busy" => "Sibuk", +"Public" => "Publik", +"Private" => "Pribadi", +"Confidential" => "Rahasia", +"Does not repeat" => "Tidak akan mengulangi", +"Daily" => "Harian", +"Weekly" => "Mingguan", +"Every Weekday" => "Setiap Hari Minggu", +"Bi-Weekly" => "Dwi-mingguan", +"Monthly" => "Bulanan", +"Yearly" => "Tahunan", +"never" => "tidak pernah", +"by occurrences" => "dari kejadian", +"by date" => "dari tanggal", +"first" => "pertama", +"second" => "kedua", +"third" => "ketiga", +"fourth" => "keempat", +"fifth" => "kelima", +"last" => "terakhir", +"Date" => "tanggal", +"Week" => "Minggu", +"Month" => "Bulan", +"List" => "daftar", +"Today" => "Hari ini", +"Settings" => "Setting", +"Your calendars" => "kalender anda", +"CalDav Link" => "tautan CalDav", +"Share Calendar" => "berbagi kalender", +"Download" => "Unduh", +"Edit" => "Sunting", +"Delete" => "hapus", +"New calendar" => "kalender baru", +"Edit calendar" => "Sunting kalender", +"Displayname" => "Namatampilan", +"Active" => "Aktif", +"Calendar color" => "Warna kalender", +"Save" => "simpan", +"Submit" => "Sampaikan", +"Cancel" => "batal", +"Edit an event" => "Sunting agenda", +"Export" => "ekspor", +"Repeating" => "pengulangan", +"Alarm" => "alarm", +"Share" => "berbagi", +"Title of the Event" => "Judul Agenda", +"Category" => "Kategori", +"Edit categories" => "Edit kategori", +"Access Class" => "Akses Kelas", +"All Day Event" => "Agenda di Semua Hari", +"From" => "Dari", +"To" => "Ke", +"Advanced options" => "opsi lanjutan", +"Location" => "Lokasi", +"Location of the Event" => "Lokasi Agenda", +"Description" => "Deskripsi", +"Description of the Event" => "Deskripsi dari Agenda", +"Repeat" => "Ulangi", +"Advanced" => "lanjutan", +"Select days" => "pilih hari", +"Select months" => "pilih bulan", +"Select weeks" => "pilih minggu", +"Interval" => "jeda", +"End" => "akhir", +"occurrences" => "kejadian", +"Please choose a calendar" => "silahkan pilih kalender", +"Take an available name!" => "pilih nama yang tersedia!", +"Import" => "impor", +"Close Dialog" => "Tutup Dialog", +"Create a new event" => "Buat agenda baru", +"Share with:" => "bagikan dengan:", +"Shared with" => "dibagikan dengan", +"Unshare" => "batalkan berbagi", +"Nobody" => "tidak seorangpun", +"Shared via calendar" => "dibagikan melalui kalender", +"of" => "dari", +"at" => "di", +"General" => "umum", +"Timezone" => "Zonawaktu", +"Update timezone automatically" => "perbaharui zona waktu secara otomatis", +"Time format" => "format waktu", +"24h" => "24 jam", +"12h" => "12 jam", +"Start week on" => "mulai minggu dari", +"Cache" => "cache", +"Clear cache for repeating events" => "bersihkan cache dari kejadian berulang", +"URLs" => "tautan", +"more info" => "lebih lanjut", +"iOS/OS X" => "iOS/OS X" +); diff --git a/l10n/is.php b/l10n/is.php new file mode 100644 index 000000000..662a37d27 --- /dev/null +++ b/l10n/is.php @@ -0,0 +1,32 @@ + "Ógild fyrirspurn", +"Sunday" => "Sunnudagur", +"Monday" => "Mánudagur", +"Tuesday" => "Þriðjudagur", +"Wednesday" => "Miðvikudagur", +"Thursday" => "Fimmtudagur", +"Friday" => "Föstudagur", +"Saturday" => "Laugardagur", +"November" => "Nóvember", +"December" => "Desember", +"Title" => "Titill", +"Clients" => "Notendahugbúnaður", +"Other" => "Annað", +"Personal" => "Um mig", +"by" => "af", +"List" => "Listi", +"Settings" => "Stillingar", +"Download" => "Niðurhal", +"Edit" => "Breyta", +"Delete" => "Eyða", +"Save" => "Vista", +"Submit" => "Senda", +"Cancel" => "Hætta við", +"Export" => "Flytja út", +"Share" => "Deila", +"Location" => "Staðsetning", +"Advanced" => "Ítarlegt", +"Import" => "Flytja inn", +"Unshare" => "Hætta deilingu", +"URLs" => "URL" +); diff --git a/l10n/it.php b/l10n/it.php new file mode 100644 index 000000000..195f5d1ca --- /dev/null +++ b/l10n/it.php @@ -0,0 +1,215 @@ + "Non tutti i calendari sono mantenuti completamente in cache", +"Everything seems to be completely cached" => "Tutto sembra essere mantenuto completamente in cache", +"No calendars found." => "Nessun calendario trovato.", +"No events found." => "Nessun evento trovato.", +"Wrong calendar" => "Calendario sbagliato", +"You do not have the permissions to edit this event." => "Non hai i permessi per modificare questo evento.", +"The file contained either no events or all events are already saved in your calendar." => "Il file non conteneva alcun evento o tutti gli eventi erano già salvati nel tuo calendario.", +"events has been saved in the new calendar" => "gli eventi sono stati salvati nel nuovo calendario", +"Import failed" => "Importazione non riuscita", +"events has been saved in your calendar" => "gli eventi sono stati salvati nel tuo calendario", +"New Timezone:" => "Nuovo fuso orario:", +"Timezone changed" => "Fuso orario cambiato", +"Invalid request" => "Richiesta non valida", +"Calendar" => "Calendario", +"Deletion failed" => "Eliminazione non riuscita", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ggg g MMMM[ aaaa]{ - [ggg g] MMMM aaaa}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ggg g MMMM[ aaaa] HH:mm{ - [ ggg g MMMM aaaa] HH:mm}", +"user" => "utente", +"group" => "gruppo", +"Editable" => "Modificabile", +"Shareable" => "Condivisibile", +"Deletable" => "Eliminabile", +"ddd" => "ddd", +"ddd M/d" => "ddd d/M", +"dddd M/d" => "dddd d/M", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, d MMM yyyy", +"Sunday" => "Domenica", +"Monday" => "Lunedì", +"Tuesday" => "Martedì", +"Wednesday" => "Mercoledì", +"Thursday" => "Giovedì", +"Friday" => "Venerdì", +"Saturday" => "Sabato", +"Sun." => "Dom.", +"Mon." => "Lun.", +"Tue." => "Mar.", +"Wed." => "Mer.", +"Thu." => "Gio.", +"Fri." => "Ven.", +"Sat." => "Sab.", +"January" => "Gennaio", +"February" => "Febbraio", +"March" => "Marzo", +"April" => "Aprile", +"May" => "Maggio", +"June" => "Giugno", +"July" => "Luglio", +"August" => "Agosto", +"September" => "Settembre", +"October" => "Ottobre", +"November" => "Novembre", +"December" => "Dicembre", +"Jan." => "Gen.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Apr.", +"May." => "Mag.", +"Jun." => "Giu.", +"Jul." => "Lug.", +"Aug." => "Ago.", +"Sep." => "Set.", +"Oct." => "Ott.", +"Nov." => "Nov.", +"Dec." => "Dic.", +"All day" => "Tutti il giorno", +"New Calendar" => "Nuovo calendario", +"Missing or invalid fields" => "Campi mancanti o non validi", +"Title" => "Titolo", +"From Date" => "Dal giorno", +"From Time" => "Ora iniziale", +"To Date" => "Al giorno", +"To Time" => "Ora finale", +"The event ends before it starts" => "L'evento finisce prima d'iniziare", +"There was a database fail" => "Si è verificato un errore del database", +"Birthday" => "Compleanno", +"Business" => "Azienda", +"Call" => "Chiama", +"Clients" => "Clienti", +"Deliverer" => "Consegna", +"Holidays" => "Vacanze", +"Ideas" => "Idee", +"Journey" => "Viaggio", +"Jubilee" => "Anniversario", +"Meeting" => "Riunione", +"Other" => "Altro", +"Personal" => "Personale", +"Projects" => "Progetti", +"Questions" => "Domande", +"Work" => "Lavoro", +"by" => "da", +"unnamed" => "senza nome", +"You do not have the permissions to update this calendar." => "Non hai i permessi per aggiornare questo calendario.", +"You do not have the permissions to delete this calendar." => "Non hai i permessi per eliminare questo calendario.", +"You do not have the permissions to add to this calendar." => "Non hai i permessi per aggiungere a questo calendario.", +"You do not have the permissions to add events to this calendar." => "Non hai i permessi per aggiungere eventi a questo calendario.", +"You do not have the permissions to delete this event." => "Non hai i permessi per eliminare questo evento.", +"Busy" => "Occupato", +"Public" => "Pubblico", +"Private" => "Privato", +"Confidential" => "Confidenziale", +"Does not repeat" => "Non ripetere", +"Daily" => "Giornaliero", +"Weekly" => "Settimanale", +"Every Weekday" => "Ogni giorno della settimana", +"Bi-Weekly" => "Ogni due settimane", +"Monthly" => "Mensile", +"Yearly" => "Annuale", +"never" => "mai", +"by occurrences" => "per occorrenze", +"by date" => "per data", +"by monthday" => "per giorno del mese", +"by weekday" => "per giorno della settimana", +"events week of month" => "settimana del mese degli eventi", +"first" => "primo", +"second" => "secondo", +"third" => "terzo", +"fourth" => "quarto", +"fifth" => "quinto", +"last" => "ultimo", +"by events date" => "per data evento", +"by yearday(s)" => "per giorno/i dell'anno", +"by weeknumber(s)" => "per numero/i settimana", +"by day and month" => "per giorno e mese", +"Date" => "Data", +"Cal." => "Cal.", +"Week" => "Settimana", +"Month" => "Mese", +"List" => "Elenco", +"Today" => "Oggi", +"Settings" => "Impostazioni", +"Your calendars" => "I tuoi calendari", +"CalDav Link" => "Collegamento CalDav", +"Share Calendar" => "Condividi calendario", +"Download" => "Scarica", +"Edit" => "Modifica", +"Delete" => "Elimina", +"New calendar" => "Nuovo calendario", +"Edit calendar" => "Modifica calendario", +"Displayname" => "Nome visualizzato", +"Active" => "Attivo", +"Calendar color" => "Colore calendario", +"Save" => "Salva", +"Submit" => "Invia", +"Cancel" => "Annulla", +"Edit an event" => "Modifica un evento", +"Export" => "Esporta", +"Eventinfo" => "Informazioni evento", +"Repeating" => "Ripetizione", +"Alarm" => "Avviso", +"Attendees" => "Partecipanti", +"Share" => "Condividi", +"Title of the Event" => "Titolo dell'evento", +"Category" => "Categoria", +"Separate categories with commas" => "Categorie separate da virgole", +"Edit categories" => "Modifica le categorie", +"Access Class" => "Classe di accesso", +"All Day Event" => "Evento che occupa tutta la giornata", +"From" => "Da", +"To" => "A", +"Advanced options" => "Opzioni avanzate", +"Location" => "Luogo", +"Location of the Event" => "Luogo dell'evento", +"Description" => "Descrizione", +"Description of the Event" => "Descrizione dell'evento", +"Repeat" => "Ripeti", +"Advanced" => "Avanzato", +"Select weekdays" => "Seleziona i giorni della settimana", +"Select days" => "Seleziona i giorni", +"and the events day of year." => "e il giorno dell'anno degli eventi.", +"and the events day of month." => "e il giorno del mese degli eventi.", +"Select months" => "Seleziona i mesi", +"Select weeks" => "Seleziona le settimane", +"and the events week of year." => "e la settimana dell'anno degli eventi.", +"Interval" => "Intervallo", +"End" => "Fine", +"occurrences" => "occorrenze", +"create a new calendar" => "Crea un nuovo calendario", +"Import a calendar file" => "Importa un file di calendario", +"Please choose a calendar" => "Scegli un calendario", +"Name of new calendar" => "Nome del nuovo calendario", +"Take an available name!" => "Usa un nome disponibile!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Un calendario con questo nome esiste già. Se continui, i due calendari saranno uniti.", +"Remove all events from the selected calendar" => "Rimuovi tutti gli eventi dal calendario selezionato", +"Import" => "Importa", +"Close Dialog" => "Chiudi la finestra di dialogo", +"Create a new event" => "Crea un nuovo evento", +"Share with:" => "Condividi con:", +"Shared with" => "Condiviso con", +"Unshare" => "Rimuovi condivisione", +"Nobody" => "Nessuno", +"Shared via calendar" => "Condiviso tramite calendario", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTA: le azioni sugli eventi condivisi tramite calendario interesseranno la condivsione dell'intero calendario.", +"View an event" => "Visualizza un evento", +"No categories selected" => "Nessuna categoria selezionata", +"of" => "di", +"at" => "alle", +"General" => "Generale", +"Timezone" => "Fuso orario", +"Update timezone automatically" => "Aggiorna automaticamente il fuso orario", +"Time format" => "Formato orario", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "La settimana inizia il", +"Cache" => "Cache", +"Clear cache for repeating events" => "Cancella gli eventi che si ripetono dalla cache", +"URLs" => "URL", +"Calendar CalDAV syncing addresses" => "Indirizzi di sincronizzazione calendari CalDAV", +"more info" => "ulteriori informazioni", +"Primary address (Kontact et al)" => "Indirizzo principale (Kontact e altri)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Collegamento(i) iCalendar sola lettura" +); diff --git a/l10n/ja_JP.php b/l10n/ja_JP.php new file mode 100644 index 000000000..29da582f0 --- /dev/null +++ b/l10n/ja_JP.php @@ -0,0 +1,215 @@ + "すべてのカレンダーは完全にキャッシュされていません", +"Everything seems to be completely cached" => "すべて完全にキャッシュされていると思われます", +"No calendars found." => "カレンダーが見つかりませんでした。", +"No events found." => "イベントが見つかりませんでした。", +"Wrong calendar" => "誤ったカレンダーです", +"You do not have the permissions to edit this event." => "このイベントを編集する権限がありません。", +"The file contained either no events or all events are already saved in your calendar." => "イベントの無いもしくはすべてのイベントを含むファイルは既にあなたのカレンダーに保存されています。", +"events has been saved in the new calendar" => "イベントは新しいカレンダーに保存されました", +"Import failed" => "インポートに失敗", +"events has been saved in your calendar" => "イベントはあなたのカレンダーに保存されました", +"New Timezone:" => "新しいタイムゾーン:", +"Timezone changed" => "タイムゾーンが変更されました", +"Invalid request" => "無効なリクエストです", +"Calendar" => "カレンダー", +"Deletion failed" => "削除に失敗しました", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "ユーザ", +"group" => "グループ", +"Editable" => "編集可能", +"Shareable" => "共有可能", +"Deletable" => "削除可能", +"ddd" => "dddd", +"ddd M/d" => "M月d日 (dddd)", +"dddd M/d" => "M月d日 (dddd)", +"MMMM yyyy" => "yyyy年M月", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "yyyy年M月d日{ '~' [yyyy年][M月]d日}", +"dddd, MMM d, yyyy" => "yyyy年M月d日 (dddd)", +"Sunday" => "日", +"Monday" => "月", +"Tuesday" => "火", +"Wednesday" => "水", +"Thursday" => "木", +"Friday" => "金", +"Saturday" => "土", +"Sun." => "日", +"Mon." => "月", +"Tue." => "火", +"Wed." => "水", +"Thu." => "木", +"Fri." => "金", +"Sat." => "土", +"January" => "1月", +"February" => "2月", +"March" => "3月", +"April" => "4月", +"May" => "5月", +"June" => "6月", +"July" => "7月", +"August" => "8月", +"September" => "9月", +"October" => "10月", +"November" => "11月", +"December" => "12月", +"Jan." => "1月", +"Feb." => "2月", +"Mar." => "3月", +"Apr." => "4月", +"May." => "5月", +"Jun." => "6月", +"Jul." => "7月", +"Aug." => "8月", +"Sep." => "9月", +"Oct." => "10月", +"Nov." => "11月", +"Dec." => "12月", +"All day" => "終日", +"New Calendar" => "新しくカレンダーを作成", +"Missing or invalid fields" => "不明もしくは無効なフィールド", +"Title" => "タイトル", +"From Date" => "開始日", +"From Time" => "開始時間", +"To Date" => "終了日", +"To Time" => "終了時間", +"The event ends before it starts" => "イベント終了時間が開始時間より先です", +"There was a database fail" => "データベースのエラーがありました", +"Birthday" => "誕生日", +"Business" => "ビジネス", +"Call" => "電話をかける", +"Clients" => "顧客", +"Deliverer" => "運送会社", +"Holidays" => "休日", +"Ideas" => "アイデア", +"Journey" => "旅行", +"Jubilee" => "記念祭", +"Meeting" => "ミーティング", +"Other" => "その他", +"Personal" => "個人", +"Projects" => "プロジェクト", +"Questions" => "質問事項", +"Work" => "週の始まり", +"by" => "による", +"unnamed" => "無名", +"You do not have the permissions to update this calendar." => "このカレンダーを更新する権限がありません。", +"You do not have the permissions to delete this calendar." => "このカレンダーを削除する権限がありません。", +"You do not have the permissions to add to this calendar." => "このカレンダーに追加する権限がありません。", +"You do not have the permissions to add events to this calendar." => "このカレンダーにイベントを追加する権限がありません。", +"You do not have the permissions to delete this event." => "このイベントを削除する権限がありません。", +"Busy" => "多忙", +"Public" => "公開", +"Private" => "プライベート", +"Confidential" => "秘密", +"Does not repeat" => "繰り返さない", +"Daily" => "毎日", +"Weekly" => "毎週", +"Every Weekday" => "毎平日", +"Bi-Weekly" => "2週間ごと", +"Monthly" => "毎月", +"Yearly" => "毎年", +"never" => "無し", +"by occurrences" => "回数で指定", +"by date" => "日付で指定", +"by monthday" => "日にちで指定", +"by weekday" => "曜日で指定", +"events week of month" => "予定のある週を指定", +"first" => "1週目", +"second" => "2週目", +"third" => "3週目", +"fourth" => "4週目", +"fifth" => "5週目", +"last" => "最終週", +"by events date" => "日付で指定", +"by yearday(s)" => "日番号で指定", +"by weeknumber(s)" => "週番号で指定", +"by day and month" => "月と日で指定", +"Date" => "日付", +"Cal." => "カレンダー", +"Week" => "週", +"Month" => "月", +"List" => "予定リスト", +"Today" => "今日", +"Settings" => "設定", +"Your calendars" => "あなたのカレンダー", +"CalDav Link" => "CalDavへのリンク", +"Share Calendar" => "カレンダーを共有する", +"Download" => "ダウンロード", +"Edit" => "編集", +"Delete" => "削除", +"New calendar" => "新しいカレンダー", +"Edit calendar" => "カレンダーを編集", +"Displayname" => "表示名", +"Active" => "アクティブ", +"Calendar color" => "カレンダーの色", +"Save" => "保存", +"Submit" => "完了", +"Cancel" => "キャンセル", +"Edit an event" => "イベントを編集", +"Export" => "エクスポート", +"Eventinfo" => "イベント情報", +"Repeating" => "繰り返し", +"Alarm" => "アラーム", +"Attendees" => "参加者", +"Share" => "共有", +"Title of the Event" => "イベントのタイトル", +"Category" => "カテゴリー", +"Separate categories with commas" => "カテゴリをコンマで区切る", +"Edit categories" => "カテゴリを編集", +"Access Class" => "公開設定", +"All Day Event" => "終日イベント", +"From" => "開始", +"To" => "終了", +"Advanced options" => "詳細設定", +"Location" => "場所", +"Location of the Event" => "イベントの場所", +"Description" => "メモ", +"Description of the Event" => "イベントの説明", +"Repeat" => "繰り返し", +"Advanced" => "詳細設定", +"Select weekdays" => "曜日を指定", +"Select days" => "日付を指定", +"and the events day of year." => "対象の年を選択する。", +"and the events day of month." => "対象の月を選択する。", +"Select months" => "月を指定する", +"Select weeks" => "週を指定する", +"and the events week of year." => "対象の週を選択する。", +"Interval" => "間隔", +"End" => "繰り返す期間", +"occurrences" => "回繰り返す", +"create a new calendar" => "新規カレンダーの作成", +"Import a calendar file" => "カレンダーファイルをインポート", +"Please choose a calendar" => "カレンダーを選択してください", +"Name of new calendar" => "新規カレンダーの名称", +"Take an available name!" => "利用可能な名前を指定してください!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "このカレンダー名はすでに使われています。もし続行する場合は、これらのカレンダーはマージされます。", +"Remove all events from the selected calendar" => "選択したカレンダーから全てのイベントを削除", +"Import" => "インポート", +"Close Dialog" => "ダイアログを閉じる", +"Create a new event" => "新しいイベントを作成", +"Share with:" => "共有:", +"Shared with" => "共有", +"Unshare" => "非共有", +"Nobody" => "無記名", +"Shared via calendar" => "カレンダー経由で共有中", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "注意: カレンダーを通して共有しているイベント上のアクションはカレンダー共有全体に影響します。", +"View an event" => "イベントを閲覧", +"No categories selected" => "カテゴリが選択されていません", +"of" => "of", +"at" => "at", +"General" => "一般", +"Timezone" => "タイムゾーン", +"Update timezone automatically" => "自動的にタイムゾーンを更新", +"Time format" => "時刻の表示形式", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "1週間の初めの曜日", +"Cache" => "キャッシュ", +"Clear cache for repeating events" => "繰り返しイベントのキャッシュをクリア", +"URLs" => "URL", +"Calendar CalDAV syncing addresses" => "CalDAVカレンダーの同期用アドレス", +"more info" => "さらに", +"Primary address (Kontact et al)" => "プライマリアドレス(コンタクト等)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "読み取り専用のiCalendarリンク" +); diff --git a/l10n/ka.php b/l10n/ka.php new file mode 100644 index 000000000..d27444a49 --- /dev/null +++ b/l10n/ka.php @@ -0,0 +1,4 @@ + "პერსონა", +"Download" => "გადმოწერა" +); diff --git a/l10n/ka_GE.php b/l10n/ka_GE.php new file mode 100644 index 000000000..e8867d2c1 --- /dev/null +++ b/l10n/ka_GE.php @@ -0,0 +1,208 @@ + "ყველა კალენდარი არ არის ქეშირებული", +"Everything seems to be completely cached" => "ყველაფერი დაქეშირებულია", +"No calendars found." => "კალენდარი არ იქნა ნაპოვნი.", +"No events found." => "ივენთი არ იქნა ნაპოვნი.", +"Wrong calendar" => "არასწორი კალენდარი", +"You do not have the permissions to edit this event." => "თქვენ არ გაქვთ მოცემული ივენთის რედაქტირების უფლება.", +"The file contained either no events or all events are already saved in your calendar." => "ფაილი არ შეიცავს ივენთებს ან ყველა ივენთი უკვე შენახულია კალენდარში", +"events has been saved in the new calendar" => "ივენთები შენახულ იქნა ახალ კალენდარში", +"Import failed" => "იმპორტი ვერ მოხერხდა", +"events has been saved in your calendar" => "ივენთები შენახულ იქნა თქვენს კალენდარში", +"New Timezone:" => "ახალი დროის სარტყელი:", +"Timezone changed" => "დროის სარტყელი შეცვლილია", +"Invalid request" => "დაუშვებელი მოთხოვნა", +"Calendar" => "კალენდარი", +"Deletion failed" => "წაშლის ველი", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "მომხმარებელი", +"group" => "ჯგუფი", +"Editable" => "რედაქტირებადი", +"Shareable" => "გაზიარებადი", +"Deletable" => "წაშლადი", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "კვირა", +"Monday" => "ორშაბათი", +"Tuesday" => "სამშაბათი", +"Wednesday" => "ოთხშაბათი", +"Thursday" => "ხუთშაბათი", +"Friday" => "პარასკევი", +"Saturday" => "შაბათი", +"Sun." => "კვ.", +"Mon." => "ორშ.", +"Tue." => "სამ.", +"Wed." => "ოთხ.", +"Thu." => "ხუთ.", +"Fri." => "პარ.", +"Sat." => "შაბ.", +"January" => "იანვარი", +"February" => "თებერვალი", +"March" => "მარტი", +"April" => "აპრილი", +"May" => "მაისი", +"June" => "ივნისი", +"July" => "ივლისი", +"August" => "აგვისტო", +"September" => "სექტემბერი", +"October" => "ოქტომბერი", +"November" => "ნოემბერი", +"December" => "დეკემბერი", +"Jan." => "იან.", +"Feb." => "თებ.", +"Mar." => "მარ.", +"Apr." => "აპრ.", +"May." => "მაი.", +"Jun." => "ივნ.", +"Jul." => "ივლ.", +"Aug." => "აგვ.", +"Sep." => "სექ.", +"Oct." => "ოქტ.", +"Nov." => "ნოე.", +"Dec." => "დეკ.", +"All day" => "ყოველ დღე", +"New Calendar" => "ახალი კალენდარი", +"Title" => "სახელი", +"From Date" => "საწყისი თარიღი", +"From Time" => "საწყისი დრო", +"To Date" => "დღემდე", +"To Time" => "დრომდე", +"The event ends before it starts" => "ივენთი გაუქმდა სანამ დაიწყებოდა", +"There was a database fail" => "ბაზის შეცდომა", +"Birthday" => "დაბადების დღე", +"Business" => "ბიზნესი", +"Call" => "Call", +"Clients" => "კლიენტები", +"Deliverer" => "მიმღებები", +"Holidays" => "არდადეგები", +"Ideas" => "იდეები", +"Journey" => "მოგზაურობა", +"Jubilee" => "იუბილე", +"Meeting" => "შეხვედრა", +"Other" => "სხვა", +"Personal" => "პირადი", +"Projects" => "პროექტები", +"Questions" => "შეკითხვები", +"Work" => "სამსახური", +"by" => "მიერ", +"unnamed" => "უსახელო", +"You do not have the permissions to update this calendar." => "თქვენ არ გაქვთ კალენდარის განახლების უფლება.", +"You do not have the permissions to delete this calendar." => "თქვენ არ გაქვთ კალენდარის წაშლის უფლება.", +"You do not have the permissions to add to this calendar." => "თქვენ არ გაქვთ კალენდარის დამატების უფლება.", +"You do not have the permissions to add events to this calendar." => "თქვენ არ გაქვთ ივენთის დამატების უფლება კალენდარში.", +"You do not have the permissions to delete this event." => "თქვენ არ გაქვთ მოცემული ივენთის წაშლის უფლება.", +"Does not repeat" => "არ უნდა გამეორდეს", +"Daily" => "ყოველდღიური", +"Weekly" => "ყოველკვირეული", +"Every Weekday" => "ყოველ კვირის დღეს", +"Bi-Weekly" => "Bi-Weekly", +"Monthly" => "ყოველთვიური", +"Yearly" => "ყოველწლიური", +"never" => "არასდროს", +"by occurrences" => "მოვლენების მიხედვით", +"by date" => "დროის მიხედვით", +"by monthday" => "თვის დღეების მიხედვით", +"by weekday" => "კვირის დღეების მიხედვით", +"events week of month" => "კვირის ას თვის ივენთები", +"first" => "პირველი", +"second" => "მეორე", +"third" => "მესამე", +"fourth" => "მეოთხე", +"fifth" => "მეხუთე", +"last" => "ბოლო", +"by events date" => "ივენთების დროის მიხედვით", +"by yearday(s)" => "ჭლის დღის(დღეების) მიხედვით", +"by weeknumber(s)" => "კვირის ნომრების მიხედვით", +"by day and month" => "დღის ან თვის მიხედვით", +"Date" => "დრო", +"Cal." => "Cal.", +"Week" => "კვირა", +"Month" => "თვე", +"List" => "სია", +"Today" => "დღეს", +"Settings" => "პარამეტრები", +"Your calendars" => "თქვენი კალენდარები", +"CalDav Link" => "CalDav ლინკი", +"Share Calendar" => "კალენდარის გაზიარება", +"Download" => "ჩამოტვირთვა", +"Edit" => "შეცვლა", +"Delete" => "წაშლა", +"New calendar" => "ახალი კალენდარი", +"Edit calendar" => "კალენდარის რედაქტირება", +"Displayname" => "დისპლეის სახელი", +"Active" => "აქტიური", +"Calendar color" => "კალენდარის ფერი", +"Save" => "შენახვა", +"Submit" => "გაგზავნა", +"Cancel" => "გაუქმება", +"Edit an event" => "ივენთის რედაქტირება", +"Export" => "ექსპორტი", +"Eventinfo" => "ივენთის ინფო", +"Repeating" => "გამეორება", +"Alarm" => "განგაში", +"Attendees" => "დამსწრეები", +"Share" => "გაზიარება", +"Title of the Event" => "ივენთის სახელი", +"Category" => "კატეგორია", +"Separate categories with commas" => "ცალკე კატეგორიები მძიმეებით", +"Edit categories" => "კატეგორიების რედაქტირება", +"All Day Event" => "ყოველდღიური ივენთი", +"From" => "დან", +"To" => "მდე", +"Advanced options" => "დამატებითი ფუნქციები", +"Location" => "ადგილმდებარეობა", +"Location of the Event" => "ივენთის ადგილმდებარეობა", +"Description" => "აღწერა", +"Description of the Event" => "ივენთის აღწერა", +"Repeat" => "გამეორება", +"Advanced" => "Advanced", +"Select weekdays" => "კვირის დღეების არჩევა", +"Select days" => "დღის არჩევა", +"and the events day of year." => "მოვლენების დღე წელიწადში.", +"and the events day of month." => "მოვლენების დღე თვეში.", +"Select months" => "აირჩიეთ თვე", +"Select weeks" => "აირჩიეთ კვირა", +"and the events week of year." => "კვირის მოვლენები წელიწადში.", +"Interval" => "ინტერვალი", +"End" => "დასასრული", +"occurrences" => "მოვლენები", +"create a new calendar" => "ახალი კალენდარის შექმნა", +"Import a calendar file" => "კალენდარის ფაილის იმპორტი", +"Please choose a calendar" => "გთხოვთ აირჩიოთ კალენდარი", +"Name of new calendar" => "ახალი კალენდარის სახელი", +"Take an available name!" => "აირჩიეთ ხელმისაწვდომი სახელი!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "კალენდარი ასეთი სახელით უკვე არსებობს. თუ გააგრძელებთ ეს კალენდარები შეერთდება.", +"Import" => "იმპორტი", +"Close Dialog" => "დიალოგის დახურვა", +"Create a new event" => "ახალი ივენთის შექმნა", +"Share with:" => "გააზიარე შემდეგით:", +"Shared with" => "გააზიარე შემდეგით", +"Unshare" => "გაუზიარებადი", +"Nobody" => "არავინ", +"Shared via calendar" => "გაზიარებულია კალენდრიდან", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "შენიშვნა: ივენთის გაზიარება კალენდრიდან იწვევს მთლიანი კალენდრის გაზიარებას.", +"View an event" => "ივენთის ნახვა", +"No categories selected" => "კატეგორია არ არის არჩეული", +"of" => "დან", +"at" => "at", +"General" => "ზოგადი", +"Timezone" => "დროის სარტყელი", +"Update timezone automatically" => "დროის სარტყლის ავტომატურად განახლება", +"Time format" => "დროის ფორმატი", +"24h" => "24სთ", +"12h" => "12სთ", +"Start week on" => "დაიწყე კვირა აქედან", +"Cache" => "ქეში", +"Clear cache for repeating events" => "განმეორებადი ივენთების წაშლა ქეშიდან", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "კალენდარის CalDAV სინქრონიზაციის მისამართი", +"more info" => "უფრო მეტი ინფორმაცია", +"Primary address (Kontact et al)" => "პირველადი მისამართი (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "წაიკითხე მხოლოდ iCalendar ლინკი(ები)" +); diff --git a/l10n/ko.php b/l10n/ko.php new file mode 100644 index 000000000..4cc4a482f --- /dev/null +++ b/l10n/ko.php @@ -0,0 +1,215 @@ + "모든 달력이 완벽히 캐시되지 않았음", +"Everything seems to be completely cached" => "모든 항목이 완벽히 캐시됨", +"No calendars found." => "달력이 없습니다.", +"No events found." => "일정이 없습니다.", +"Wrong calendar" => "잘못된 달력", +"You do not have the permissions to edit this event." => "이 일정을 편집할 수 있는 권한이 없습니다.", +"The file contained either no events or all events are already saved in your calendar." => "이 파일에 일정 정보가 없거나 모든 정보가 달력에 저장되었습니다.", +"events has been saved in the new calendar" => "일정이 새로운 달력에 저장되었습니다", +"Import failed" => "가져오기 실패", +"events has been saved in your calendar" => "일정이 달력에 저장됨", +"New Timezone:" => "새 시간대:", +"Timezone changed" => "시간대 변경됨", +"Invalid request" => "잘못된 요청", +"Calendar" => "달력", +"Deletion failed" => "삭제 실패", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "[yyyy ]MMMM d ddd{- yyyy MMMM[ d ddd]}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "[yyyy ]MMMM d ddd HH:mm{ - [yyyy MMMM d ddd] HH:mm}", +"user" => "사용자", +"group" => "그룹", +"Editable" => "편집 가능", +"Shareable" => "공유 가능", +"Deletable" => "삭제 가능", +"ddd" => "ddd", +"ddd M/d" => "M/d ddd", +"dddd M/d" => "M/d dddd", +"MMMM yyyy" => "yyyy MMMM", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "[yyyy] MMM d{ '—' [yyyy] MMM d}", +"dddd, MMM d, yyyy" => "yyyy MMM d dddd", +"Sunday" => "일요일", +"Monday" => "월요일", +"Tuesday" => "화요일", +"Wednesday" => "수요일", +"Thursday" => "목요일", +"Friday" => "금요일", +"Saturday" => "토요일", +"Sun." => "일", +"Mon." => "월", +"Tue." => "화", +"Wed." => "수", +"Thu." => "목", +"Fri." => "금", +"Sat." => "토", +"January" => "1월", +"February" => "2월", +"March" => "3월", +"April" => "4월", +"May" => "5월", +"June" => "6월", +"July" => "7월", +"August" => "8월", +"September" => "9월", +"October" => "10월", +"November" => "11월", +"December" => "12월", +"Jan." => "1월", +"Feb." => "2월", +"Mar." => "3월", +"Apr." => "4월", +"May." => "5월", +"Jun." => "6월", +"Jul." => "7월", +"Aug." => "8월", +"Sep." => "9월", +"Oct." => "10월", +"Nov." => "11월", +"Dec." => "12월", +"All day" => "매일", +"New Calendar" => "새 달력", +"Missing or invalid fields" => "누락되었거나 잘못된 필드", +"Title" => "제목", +"From Date" => "시작 날짜", +"From Time" => "시작 시간", +"To Date" => "종료 날짜", +"To Time" => "종료 시간", +"The event ends before it starts" => "종료 일시가 시작 일시보다 빠름", +"There was a database fail" => "데이터베이스 오류가 발생함", +"Birthday" => "생일", +"Business" => "사업", +"Call" => "통화", +"Clients" => "클라이언트", +"Deliverer" => "배송", +"Holidays" => "공휴일", +"Ideas" => "생각", +"Journey" => "여행", +"Jubilee" => "기념일", +"Meeting" => "미팅", +"Other" => "기타", +"Personal" => "개인", +"Projects" => "프로젝트", +"Questions" => "질문", +"Work" => "작업", +"by" => "로", +"unnamed" => "이름 없음", +"You do not have the permissions to update this calendar." => "이 달력을 업데이트할 수 있는 권한이 없습니다.", +"You do not have the permissions to delete this calendar." => "이 달력을 삭제할 수 있는 권한이 없습니다.", +"You do not have the permissions to add to this calendar." => "이 달력에 추가할 수 있는 권한이 없습니다.", +"You do not have the permissions to add events to this calendar." => "이 달력에 일정을 추가할 수 있는 권한이 없습니다.", +"You do not have the permissions to delete this event." => "이 일정을 삭제할 수 있는 권한이 없습니다.", +"Busy" => "바쁨", +"Public" => "공개", +"Private" => "개인", +"Confidential" => "비밀", +"Does not repeat" => "반복 없음", +"Daily" => "매일", +"Weekly" => "매주", +"Every Weekday" => "매주 특정 요일", +"Bi-Weekly" => "2주마다", +"Monthly" => "매월", +"Yearly" => "매년", +"never" => "없음", +"by occurrences" => "특정 횟수 이후", +"by date" => "지정한 날짜까지", +"by monthday" => "지정한 달까지", +"by weekday" => "지정한 요일까지", +"events week of month" => "이달의 한 주 일정", +"first" => "첫번째", +"second" => "두번째", +"third" => "세번째", +"fourth" => "네번째", +"fifth" => "다섯번째", +"last" => "마지막", +"by events date" => "이벤트 날짜 순", +"by yearday(s)" => "날짜 번호 순", +"by weeknumber(s)" => "주 번호 순", +"by day and month" => "날짜 순", +"Date" => "날짜", +"Cal." => "달력", +"Week" => "주", +"Month" => "달", +"List" => "목록", +"Today" => "오늘", +"Settings" => "설정", +"Your calendars" => "내 달력", +"CalDav Link" => "CalDAV 링크", +"Share Calendar" => "달력 공유", +"Download" => "다운로드", +"Edit" => "편집", +"Delete" => "삭제", +"New calendar" => "새 달력", +"Edit calendar" => "달력 편집", +"Displayname" => "표시 이름", +"Active" => "활성", +"Calendar color" => "달력 색상", +"Save" => "저장", +"Submit" => "보내기", +"Cancel" => "취소", +"Edit an event" => "일정 편집", +"Export" => "내보내기", +"Eventinfo" => "일정 정보", +"Repeating" => "반복", +"Alarm" => "알람", +"Attendees" => "참석자", +"Share" => "공유", +"Title of the Event" => "일정 제목", +"Category" => "분류", +"Separate categories with commas" => "쉼표로 분류 구분", +"Edit categories" => "분류 수정", +"Access Class" => "접근 등급", +"All Day Event" => "종일 행사", +"From" => "시작", +"To" => "끝", +"Advanced options" => "고급 설정", +"Location" => "위치", +"Location of the Event" => "일정 위치", +"Description" => "설명", +"Description of the Event" => "일정 설명", +"Repeat" => "반복", +"Advanced" => "고급", +"Select weekdays" => "요일 선택", +"Select days" => "날짜 선택", +"and the events day of year." => "그리고 이 해의 일정", +"and the events day of month." => "그리고 이 달의 일정", +"Select months" => "달 선택", +"Select weeks" => "주 선택", +"and the events week of year." => "그리고 이 해의 주간 일정", +"Interval" => "간격", +"End" => "끝", +"occurrences" => "번 이후", +"create a new calendar" => "새 달력 만들기", +"Import a calendar file" => "달력 파일 가져오기", +"Please choose a calendar" => "달력을 선택하십시오", +"Name of new calendar" => "새 달력 이름", +"Take an available name!" => "사용 가능한 이름을 입력하십시오!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "같은 이름으로 된 달력이 이미 존재합니다. 계속 진행하면 기존 달력에 합쳐집니다.", +"Remove all events from the selected calendar" => "선택한 달력의 모든 일정 삭제", +"Import" => "가져오기", +"Close Dialog" => "대화 상자 닫기", +"Create a new event" => "새 일정 만들기", +"Share with:" => "다음과 공유됨:", +"Shared with" => "다음과 공유됨:", +"Unshare" => "공유 해제", +"Nobody" => "아무도 없음", +"Shared via calendar" => "달력으로 공유됨", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "메모: 달력으로 공유된 행사는 전체 달력 공유에 영향을 줍니다.", +"View an event" => "일정 보기", +"No categories selected" => "선택된 분류 없음", +"of" => "의", +"at" => "에서", +"General" => "일반", +"Timezone" => "시간대", +"Update timezone automatically" => "자동으로 시간대 업데이트", +"Time format" => "시간 형식", +"24h" => "24시간", +"12h" => "12시간", +"Start week on" => "주 시작 요일", +"Cache" => "캐시", +"Clear cache for repeating events" => "일정 반복을 위한 캐시 삭제", +"URLs" => "URL", +"Calendar CalDAV syncing addresses" => "CalDAV 달력 동기화 주소", +"more info" => "더 많은 정보", +"Primary address (Kontact et al)" => "기본 주소 (Kontact 등)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "iCalendar 링크만 읽기" +); diff --git a/l10n/ku_IQ.php b/l10n/ku_IQ.php new file mode 100644 index 000000000..9e0a27d2d --- /dev/null +++ b/l10n/ku_IQ.php @@ -0,0 +1,10 @@ + "ناونیشان", +"unnamed" => "بێ ناو", +"Settings" => "ده‌ستكاری", +"Download" => "داگرتن", +"Save" => "پاشکه‌وتکردن", +"Submit" => "ناردن", +"Export" => "هه‌ناردن", +"Import" => "هێنان" +); diff --git a/l10n/lb.php b/l10n/lb.php new file mode 100644 index 000000000..8f48c5bda --- /dev/null +++ b/l10n/lb.php @@ -0,0 +1,191 @@ + "Keng Kalenner fonnt.", +"No events found." => "Keng Evenementer fonnt.", +"Wrong calendar" => "Falschen Kalenner", +"Import failed" => "Import ass feelgeschloen", +"New Timezone:" => "Nei Zäitzone:", +"Timezone changed" => "Zäitzon geännert", +"Invalid request" => "Ongülteg Requête", +"Calendar" => "Kalenner", +"Deletion failed" => "Konnt net läschen", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "Benotzer", +"group" => "Grupp", +"Shareable" => "Deelbar", +"Deletable" => "Läschbar", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Sonndes", +"Monday" => "Méindes", +"Tuesday" => "Dënschdes", +"Wednesday" => "Mëttwoch", +"Thursday" => "Donneschdes", +"Friday" => "Freides", +"Saturday" => "Samschdes", +"Sun." => "So. ", +"Mon." => "Méin. ", +"Tue." => "Dën.", +"Wed." => "Mëtt.", +"Thu." => "Do.", +"Fri." => "Fr.", +"Sat." => "Sam.", +"January" => "Januar", +"February" => "Februar", +"March" => "Mäerz", +"April" => "Abrëll", +"May" => "Mee", +"June" => "Juni", +"July" => "Juli", +"August" => "August", +"September" => "September", +"October" => "Oktober", +"November" => "November", +"December" => "Dezember", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mär.", +"Apr." => "Abr.", +"May." => "Mäi.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dez.", +"All day" => "All Dag", +"New Calendar" => "Neien Kalenner", +"Title" => "Titel", +"From Date" => "Vun Datum", +"From Time" => "Vun Zäit", +"To Date" => "Bis Datum", +"To Time" => "Bis Zäit", +"The event ends before it starts" => "D'Evenement hält op ier et ufänkt", +"There was a database fail" => "En Datebank Feeler ass opgetrueden", +"Birthday" => "Gebuertsdag", +"Business" => "Geschäftlech", +"Call" => "Uruff", +"Clients" => "Clienten", +"Deliverer" => "Liwwerant", +"Holidays" => "Vakanzen", +"Ideas" => "Ideeën", +"Journey" => "Dag", +"Jubilee" => "Jubiläum", +"Meeting" => "Meeting", +"Other" => "Aner", +"Personal" => "Perséinlech", +"Projects" => "Projeten", +"Questions" => "Froen", +"Work" => "Aarbecht", +"unnamed" => "ouni Numm", +"Busy" => "Beschäftegt", +"Public" => "Ëffentlech", +"Private" => "Privat", +"Confidential" => "Confidentiel", +"Does not repeat" => "Widderhëlt sech net", +"Daily" => "Deeglech", +"Weekly" => "All Woch", +"Every Weekday" => "All Wochendag", +"Bi-Weekly" => "All zweet Woch", +"Monthly" => "All Mount", +"Yearly" => "All Joer", +"never" => "ni", +"by occurrences" => "no Virkommes", +"by date" => "no Datum", +"by monthday" => "no Mount-Dag", +"by weekday" => "no Wochendag", +"events week of month" => "Events Woch vum mount", +"first" => "éischt", +"second" => "Sekonn", +"third" => "Drëtt", +"fourth" => "Féiert", +"fifth" => "Fënneft", +"last" => "Läscht", +"by events date" => "no Events Datum", +"by yearday(s)" => "no Joerdag/deeg", +"by weeknumber(s)" => "no Wochenummer(en)", +"by day and month" => "no Dag a Mount", +"Date" => "Datum", +"Cal." => "Cal.", +"Week" => "Woch", +"Month" => "Mount", +"List" => "Lescht", +"Today" => "Haut", +"Settings" => "Astellungen", +"Your calendars" => "Deng Kalenneren", +"CalDav Link" => "CalDav Link", +"Share Calendar" => "Kalenner deelen", +"Download" => "Eroflueden", +"Edit" => "Editéieren", +"Delete" => "Läschen", +"New calendar" => "Neien Kalenner", +"Edit calendar" => "Kalenner editéieren", +"Displayname" => "Numm", +"Active" => "Aktiv", +"Calendar color" => "Fuerf vum Kalenner", +"Save" => "Späicheren", +"Submit" => "Fortschécken", +"Cancel" => "Ofbriechen", +"Edit an event" => "Evenement editéieren", +"Export" => "Export", +"Eventinfo" => "Event Info", +"Repeating" => "Widderhëlt sech", +"Alarm" => "Alarm", +"Attendees" => "Participanten", +"Share" => "Deelen", +"Title of the Event" => "Titel vum Evenement", +"Category" => "Kategorie", +"Edit categories" => "Kategorien editéieren", +"Access Class" => "Accès Klassen", +"All Day Event" => "Ganz-Dag Evenement", +"From" => "Vun", +"To" => "Fir", +"Advanced options" => "Avancéiert Optiounen", +"Location" => "Uert", +"Location of the Event" => "Uert vum Evenement", +"Description" => "Beschreiwung", +"Description of the Event" => "Beschreiwung vum Evenement", +"Repeat" => "Widderhuelen", +"Advanced" => "Erweidert", +"Select weekdays" => "Wochendeeg auswielen", +"Select days" => "Deeg auswielen", +"and the events day of year." => "an den Events Dag vum Joer.", +"and the events day of month." => "an den Events Dag vum Mount.", +"Select months" => "Méint auswielen", +"Select weeks" => "Wochen auswielen", +"and the events week of year." => "an den d'Events Woch vum Joer.", +"Interval" => "Intervall", +"End" => "Enn", +"occurrences" => "Virkommes", +"create a new calendar" => "E neie Kalenner uleeën", +"Import a calendar file" => "E Kalenner Fichier importéieren", +"Please choose a calendar" => "Wien e Kalenner aus", +"Name of new calendar" => "Numm vum neie Kalenner", +"Take an available name!" => "Huel en disponibele Numm!", +"Import" => "Import", +"Close Dialog" => "Dialog zoumaachen", +"Create a new event" => "En Evenement maachen", +"Share with:" => "Deelen mat:", +"Shared with" => "Gedeelt mat:", +"Unshare" => "Net méi deelen", +"Nobody" => "Keen", +"Shared via calendar" => "Iwwer Kalenner deelen", +"No categories selected" => "Keng Kategorien ausgewielt", +"of" => "vun", +"General" => "Allgemeng", +"Timezone" => "Zäitzon", +"Update timezone automatically" => "Zäitzon automatesch updaten", +"Time format" => "Zäit Format", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Woch ufänken um", +"Cache" => "Cache", +"URLs" => "URLs", +"more info" => "méi Informatiounen", +"iOS/OS X" => "iOS/OS X" +); diff --git a/l10n/lt_LT.php b/l10n/lt_LT.php new file mode 100644 index 000000000..dfe552dca --- /dev/null +++ b/l10n/lt_LT.php @@ -0,0 +1,145 @@ + "Kalendorių nerasta.", +"No events found." => "Įvykių nerasta.", +"Wrong calendar" => "Ne tas kalendorius", +"New Timezone:" => "Nauja laiko juosta:", +"Timezone changed" => "Laiko zona pakeista", +"Invalid request" => "Klaidinga užklausa", +"Calendar" => "Kalendorius", +"Deletion failed" => "Ištrinti nepavyko", +"Editable" => "Redaguojamas", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"Sunday" => "Sekmadienis", +"Monday" => "Pirmadienis", +"Tuesday" => "Antradienis", +"Wednesday" => "Trečiadienis", +"Thursday" => "Ketvirtadienis", +"Friday" => "Penktadienis", +"Saturday" => "Šeštadienis", +"Sun." => "Sek.", +"Mon." => "Pr.", +"Tue." => "An.", +"Wed." => "Tr.", +"Thu." => "Ket.", +"Fri." => "Pen.", +"Sat." => "Šešt.", +"January" => "Sausis", +"February" => "Vasaris", +"March" => "Kovas", +"April" => "Balandis", +"May" => "Gegužė", +"June" => "Birželis", +"July" => "Liepa", +"August" => "Rugpjūtis", +"September" => "Rugsėjis", +"October" => "Spalis", +"November" => "Lapkritis", +"December" => "Gruodis", +"Jan." => "Sau.", +"Feb." => "Vas.", +"Mar." => "Kov.", +"Apr." => "Bal.", +"May." => "Geg.", +"Jun." => "Bir.", +"Jul." => "Lie.", +"Aug." => "Rugp.", +"Sep." => "Rugs.", +"Oct." => "Spa.", +"Nov." => "Lap.", +"Dec." => "Groud.", +"All day" => "Visa diena", +"New Calendar" => "Naujas kalendorius", +"Title" => "Pavadinimas", +"From Date" => "Nuo datos", +"From Time" => "Nuo laiko", +"To Date" => "Iki datos", +"To Time" => "Iki laiko", +"The event ends before it starts" => "Įvykis baigiasi anksčiau nei jis prasideda", +"There was a database fail" => "Įvyko duomenų bazės klaida", +"Birthday" => "Gimtadienis", +"Business" => "Verslas", +"Call" => "Skambučiai", +"Clients" => "Klientai", +"Deliverer" => "Vykdytojas", +"Holidays" => "Išeiginės", +"Ideas" => "Idėjos", +"Journey" => "Kelionė", +"Jubilee" => "Jubiliejus", +"Meeting" => "Susitikimas", +"Other" => "Kiti", +"Personal" => "Asmeniniai", +"Projects" => "Projektai", +"Questions" => "Klausimai", +"Work" => "Darbas", +"unnamed" => "be pavadinimo", +"Does not repeat" => "Nekartoti", +"Daily" => "Kasdien", +"Weekly" => "Kiekvieną savaitę", +"Every Weekday" => "Kiekvieną savaitės dieną", +"Bi-Weekly" => "Kas dvi savaites", +"Monthly" => "Kiekvieną mėnesį", +"Yearly" => "Kiekvienais metais", +"never" => "niekada", +"by date" => "pagal datą", +"by monthday" => "pagal mėnesio dieną", +"by weekday" => "pagal savaitės dieną", +"Date" => "Data", +"Cal." => "Kal.", +"Week" => "Savaitė", +"Month" => "Mėnuo", +"List" => "Sąrašas", +"Today" => "Šiandien", +"Settings" => "Nustatymai", +"Your calendars" => "Jūsų kalendoriai", +"CalDav Link" => "CalDav adresas", +"Share Calendar" => "Dalintis kalendoriumi", +"Download" => "Atsisiųsti", +"Edit" => "Keisti", +"Delete" => "Trinti", +"New calendar" => "Naujas kalendorius", +"Edit calendar" => "Taisyti kalendorių", +"Displayname" => "Pavadinimas", +"Active" => "Naudojamas", +"Calendar color" => "Kalendoriaus spalva", +"Save" => "Išsaugoti", +"Submit" => "Išsaugoti", +"Cancel" => "Atšaukti", +"Edit an event" => "Taisyti įvykį", +"Export" => "Eksportuoti", +"Eventinfo" => "Informacija", +"Repeating" => "Pasikartojantis", +"Alarm" => "Priminimas", +"Attendees" => "Dalyviai", +"Share" => "Dalintis", +"Title of the Event" => "Įvykio pavadinimas", +"Category" => "Kategorija", +"Separate categories with commas" => "Atskirkite kategorijas kableliais", +"Edit categories" => "Redaguoti kategorijas", +"All Day Event" => "Visos dienos įvykis", +"From" => "Nuo", +"To" => "Iki", +"Advanced options" => "Papildomi nustatymai", +"Location" => "Vieta", +"Location of the Event" => "Įvykio vieta", +"Description" => "Aprašymas", +"Description of the Event" => "Įvykio aprašymas", +"Repeat" => "Kartoti", +"Select weekdays" => "Pasirinkite savaitės dienas", +"Select days" => "Pasirinkite dienas", +"Select months" => "Pasirinkite mėnesius", +"Select weeks" => "Pasirinkite savaites", +"Interval" => "Intervalas", +"End" => "Pabaiga", +"create a new calendar" => "sukurti naują kalendorių", +"Import a calendar file" => "Importuoti kalendoriaus failą", +"Name of new calendar" => "Naujo kalendoriaus pavadinimas", +"Import" => "Importuoti", +"Close Dialog" => "Uždaryti", +"Create a new event" => "Sukurti naują įvykį", +"Unshare" => "Nebesidalinti", +"View an event" => "Peržiūrėti įvykį", +"No categories selected" => "Nepasirinktos jokios katagorijos", +"Timezone" => "Laiko juosta", +"24h" => "24val", +"12h" => "12val" +); diff --git a/l10n/lv.php b/l10n/lv.php new file mode 100644 index 000000000..b11445c1d --- /dev/null +++ b/l10n/lv.php @@ -0,0 +1,215 @@ + "Ne visi kalendāri ir pilnībā pieglabāti", +"Everything seems to be completely cached" => "Izskatās, ka viss ir pilnībā pieglabāts", +"No calendars found." => "Nav atrastu kalendāru.", +"No events found." => "Nav atrastu notikumu.", +"Wrong calendar" => "Nepareizs kalendārs", +"You do not have the permissions to edit this event." => "Jums nav tiesību rediģēt šo notikumu.", +"The file contained either no events or all events are already saved in your calendar." => "Datne nesaturēja nevienu notikumu, vai arī visi notikumi jau ir saglabāti jūsu kalendārā,", +"events has been saved in the new calendar" => "notikumi ir saglabāti jaunajā kalendārā", +"Import failed" => "neizdevās importēt", +"events has been saved in your calendar" => "notikumi tika saglabāti jūsu kalendārā", +"New Timezone:" => "Jauna laika josla", +"Timezone changed" => "Laika josla ir nomainīta", +"Invalid request" => "Nederīgs vaicājums", +"Calendar" => "Kalendārs", +"Deletion failed" => "Neizdevās izdzēst", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "lietotājs", +"group" => "grupa", +"Editable" => "Rediģējams", +"Shareable" => "Var dalīties", +"Deletable" => "Dzēšams", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Svētdiena", +"Monday" => "Pirmdiena", +"Tuesday" => "Otrdiena", +"Wednesday" => "Trešdiena", +"Thursday" => "Ceturtdiena", +"Friday" => "Piektdiena", +"Saturday" => "Sestdiena", +"Sun." => "Sv.", +"Mon." => "Pr.", +"Tue." => "Ot.", +"Wed." => "Tr.", +"Thu." => "Ce.", +"Fri." => "Pk.", +"Sat." => "Se.", +"January" => "Janvāris", +"February" => "Februāris", +"March" => "Marts", +"April" => "Aprīlis", +"May" => "Maijs", +"June" => "Jūnijs", +"July" => "Jūlijs", +"August" => "Augusts", +"September" => "Septembris", +"October" => "Oktobris", +"November" => "Novembris", +"December" => "Decembris", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Apr.", +"May." => "Mai.", +"Jun." => "Jūn.", +"Jul." => "Jūl.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dec.", +"All day" => "Visu dienu", +"New Calendar" => "Jauns kalendārs", +"Missing or invalid fields" => "Trūkstoši vai nederīgi lauki", +"Title" => "Nosaukums", +"From Date" => "Sākuma datums", +"From Time" => "Sākuma laiks", +"To Date" => "Beigu datums", +"To Time" => "Beigu laiks", +"The event ends before it starts" => "Notikums beidzas vēl nesācies", +"There was a database fail" => "Datubāzes kļūme", +"Birthday" => "Dzimšanas diena", +"Business" => "Darījumi", +"Call" => "Zvans", +"Clients" => "Klienti", +"Deliverer" => "Piegādātājs", +"Holidays" => "Brīvdienas", +"Ideas" => "Idejas", +"Journey" => "Ceļojums", +"Jubilee" => "Jubileja", +"Meeting" => "Tikšanās", +"Other" => "Cits", +"Personal" => "Personīgi", +"Projects" => "Projekti", +"Questions" => "Jautājumi", +"Work" => "Darbs", +"by" => "līdz", +"unnamed" => "nenosaukts", +"You do not have the permissions to update this calendar." => "Jums nav tiesību atjaunināt šo kalendāru.", +"You do not have the permissions to delete this calendar." => "Jums nav tiesību dzēst šo kalendāru.", +"You do not have the permissions to add to this calendar." => "Jums nav tiesību pievienot šim kalendāram.", +"You do not have the permissions to add events to this calendar." => "Jums nav tiesību pievienot notikumus šim kalendāram.", +"You do not have the permissions to delete this event." => "Jums nav tiesību dzēst šo notikumu.", +"Busy" => "Aizņemts", +"Public" => "Publisks", +"Private" => "Privāts", +"Confidential" => "Konfidenciāls", +"Does not repeat" => "Neatkārtojas", +"Daily" => "Katru dienu", +"Weekly" => "Katru nedēļu", +"Every Weekday" => "Katrā nedēļas dienā", +"Bi-Weekly" => "Katru otro nedēļu", +"Monthly" => "Katru mēnesi", +"Yearly" => "Katru gadu", +"never" => "nekad", +"by occurrences" => "pēc reižu skaita", +"by date" => "līdz datumam", +"by monthday" => "pēc mēneša dienas", +"by weekday" => "pēc nedēļas dienas", +"events week of month" => "notikuma mēneša nedēļa", +"first" => "pirmais", +"second" => "otrais", +"third" => "trešais", +"fourth" => "ceturtais", +"fifth" => "piektais", +"last" => "pēdējais", +"by events date" => "pēc notikumu datuma", +"by yearday(s)" => "pēc gada dienas(-ām)", +"by weeknumber(s)" => "pēc nedēļas numura(-iem)", +"by day and month" => "pēc dienas un mēneša", +"Date" => "Datums", +"Cal." => "Kal.", +"Week" => "Nedēļa", +"Month" => "Mēnesis", +"List" => "Saraksts", +"Today" => "Šodien", +"Settings" => "Iestatījumi", +"Your calendars" => "Jūsu kalendāri", +"CalDav Link" => "CalDav saite", +"Share Calendar" => "Dalīties ar kalendāru", +"Download" => "Lejupielādēt", +"Edit" => "Rediģēt", +"Delete" => "Dzēst", +"New calendar" => "Jauns kalendārs", +"Edit calendar" => "Rediģēt kalendāru", +"Displayname" => "Redzamais vārds", +"Active" => "Aktīvs", +"Calendar color" => "Kalendāra krāsa", +"Save" => "Saglabāt", +"Submit" => "Iesniegt", +"Cancel" => "Atcelt", +"Edit an event" => "Rediģēt notikumu", +"Export" => "Eksportēt", +"Eventinfo" => "Notikuma info", +"Repeating" => "Atkārtojas", +"Alarm" => "Signāls", +"Attendees" => "Apmeklētāji", +"Share" => "Dalīties", +"Title of the Event" => "Notikuma nosaukums", +"Category" => "Kategorija", +"Separate categories with commas" => "Atdalīt kategorijas ar komatiem", +"Edit categories" => "Rediģēt kategoriju", +"Access Class" => "Pieejas klase", +"All Day Event" => "Visas dienas notikums", +"From" => "No", +"To" => "Līdz", +"Advanced options" => "Paplašinātās opcijas", +"Location" => "Vieta", +"Location of the Event" => "Notikuma vieta", +"Description" => "Apraksts", +"Description of the Event" => "Notikuma apraksts", +"Repeat" => "Atkārtot", +"Advanced" => "Paplašināti", +"Select weekdays" => "Izvēlieties nedēļas dienas", +"Select days" => "Izvēlieties dienas", +"and the events day of year." => "un notikuma gada dienu.", +"and the events day of month." => "un notikuma mēneša dienu.", +"Select months" => "Izvēlieties mēnešus", +"Select weeks" => "Izvēlieties nedēļas", +"and the events week of year." => "un notikuma gada nedēļu.", +"Interval" => "Intervāls", +"End" => "Beigas", +"occurrences" => "notikumu skaits", +"create a new calendar" => "izveidot jaunu kalendāru", +"Import a calendar file" => "Importēt kalendāra datni", +"Please choose a calendar" => "Lūdzu, izvēlieties kalendāru", +"Name of new calendar" => "Jaunā kalendāra nosaukums", +"Take an available name!" => "Paņemiet pieejamu nosaukumu!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Kalendārs ar tādu nosaukumu jau eksistē. Ja tomēr turpināsiet, šie kalendāri tiks apvienoti.", +"Remove all events from the selected calendar" => "Izņemt visus notikumus no izvēlētā kalendāra", +"Import" => "Importēt", +"Close Dialog" => "Aizvērt dialoglodziņu", +"Create a new event" => "Izveidot jaunu notikumu", +"Share with:" => "Dalīties ar:", +"Shared with" => "Dalīt ar", +"Unshare" => "Pārtraukt dalīšanos", +"Nobody" => "Neviens", +"Shared via calendar" => "Koplietots caur kalendāru", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "PIEZĪME — darbības vai notikumi, kas koplietoti caur kalendāru ietekmēs visa kalendāra koplietošanu.", +"View an event" => "Skatīt notikumu", +"No categories selected" => "Nav izvēlētu kategoriju", +"of" => "no", +"at" => " ", +"General" => "Vispārīgās", +"Timezone" => "Laika josla", +"Update timezone automatically" => "Automātiski atjaunināt laika joslas", +"Time format" => "Laika formāts", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Sākt nedēļu ar", +"Cache" => "Kešatmiņa", +"Clear cache for repeating events" => "Attīrīt kešatmiņu no notikumiem, kas atkārtojas", +"URLs" => "URL", +"Calendar CalDAV syncing addresses" => "Kalendāra CalDAV sinhronizēšanas adreses", +"more info" => "vairāk informācijas", +"Primary address (Kontact et al)" => "Primārā adrese", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Lasīt tikai iCalendar saiti(-es)" +); diff --git a/l10n/mk.php b/l10n/mk.php new file mode 100644 index 000000000..3de405226 --- /dev/null +++ b/l10n/mk.php @@ -0,0 +1,210 @@ + "Не сите календари се целосно кеширани", +"Everything seems to be completely cached" => "Сѐ изгледа дека е целосно кеширано", +"No calendars found." => "Не се најдени календари.", +"No events found." => "Не се најдени настани.", +"Wrong calendar" => "Погрешен календар", +"You do not have the permissions to edit this event." => "Немате привилегија да го уредувате настанов.", +"The file contained either no events or all events are already saved in your calendar." => "Датотеката или не содржи настани или сите настани се веќе во Вашиот календар.", +"events has been saved in the new calendar" => "Настани беа снимени во нов календар", +"Import failed" => "Внесувањето не успеа", +"events has been saved in your calendar" => "настани беа снимени во Вашиот календар", +"New Timezone:" => "Нова временска зона:", +"Timezone changed" => "Временската зона е променета", +"Invalid request" => "Неправилно барање", +"Calendar" => "Календар", +"Deletion failed" => "Бришењето е неуспешно", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "корисник", +"group" => "група", +"Editable" => "Изменливо", +"Shareable" => "Се споделува", +"Deletable" => "Се брише", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Недела", +"Monday" => "Понеделник", +"Tuesday" => "Вторник", +"Wednesday" => "Среда", +"Thursday" => "Четврток", +"Friday" => "Петок", +"Saturday" => "Сабота", +"Sun." => "Нед.", +"Mon." => "Пон.", +"Tue." => "Вто.", +"Wed." => "Сре.", +"Thu." => "Чет.", +"Fri." => "Пет.", +"Sat." => "Саб.", +"January" => "Јануари", +"February" => "Февруари", +"March" => "Март", +"April" => "Април", +"May" => "Мај", +"June" => "Јуни", +"July" => "Јули", +"August" => "Август", +"September" => "Септември", +"October" => "Октомври", +"November" => "Ноември", +"December" => "Декември", +"Jan." => "Јан.", +"Feb." => "Фев.", +"Mar." => "Мар.", +"Apr." => "Апр.", +"May." => "Мај.", +"Jun." => "Јун.", +"Jul." => "Јул.", +"Aug." => "Авг.", +"Sep." => "Сеп.", +"Oct." => "Окт.", +"Nov." => "Ное.", +"Dec." => "Дек.", +"All day" => "Цел ден", +"New Calendar" => "Нов календар", +"Missing or invalid fields" => "Непостоечки или неправилни полиња", +"Title" => "Наслов", +"From Date" => "Од датум", +"From Time" => "Од време", +"To Date" => "До датум", +"To Time" => "До време", +"The event ends before it starts" => "Овој настан завршува пред за почне", +"There was a database fail" => "Имаше проблем со базата", +"Birthday" => "Роденден", +"Business" => "Деловно", +"Call" => "Повикај", +"Clients" => "Клиенти", +"Deliverer" => "Доставувач", +"Holidays" => "Празници", +"Ideas" => "Идеи", +"Journey" => "Патување", +"Jubilee" => "Јубилеј", +"Meeting" => "Состанок", +"Other" => "Останато", +"Personal" => "Лично", +"Projects" => "Проекти", +"Questions" => "Прашања", +"Work" => "Работа", +"by" => "од", +"unnamed" => "неименувано", +"You do not have the permissions to update this calendar." => "Немате привилегија да го ажурирајте календаров.", +"You do not have the permissions to delete this calendar." => "Немате привилегија да го избришете календаров.", +"You do not have the permissions to add to this calendar." => "Немате привилегија да додадете во календаров.", +"You do not have the permissions to add events to this calendar." => "Немате привилегија да додавате настани во овој календар.", +"You do not have the permissions to delete this event." => "Немате привилегија да го избришете настанов.", +"Does not repeat" => "Не се повторува", +"Daily" => "Дневно", +"Weekly" => "Седмично", +"Every Weekday" => "Секој работен ден", +"Bi-Weekly" => "Дво-седмично", +"Monthly" => "Месечно", +"Yearly" => "Годишно", +"never" => "никогаш", +"by occurrences" => "по настан", +"by date" => "по датум", +"by monthday" => "по ден во месецот", +"by weekday" => "по работен ден", +"events week of month" => "седмични настани од месец", +"first" => "прв", +"second" => "втор", +"third" => "трет", +"fourth" => "четврт", +"fifth" => "пет", +"last" => "последен", +"by events date" => "по датумот на настанот", +"by yearday(s)" => "по вчерашните", +"by weeknumber(s)" => "по број на седмицата", +"by day and month" => "по ден и месец", +"Date" => "Датум", +"Cal." => "Кал.", +"Week" => "Седмица", +"Month" => "Месец", +"List" => "Листа", +"Today" => "Денеска", +"Settings" => "Параметри", +"Your calendars" => "Ваши календари", +"CalDav Link" => "Врска за CalDav", +"Share Calendar" => "Сподели календар", +"Download" => "Преземи", +"Edit" => "Уреди", +"Delete" => "Избриши", +"New calendar" => "Нов календар", +"Edit calendar" => "Уреди календар", +"Displayname" => "Име за приказ", +"Active" => "Активен", +"Calendar color" => "Боја на календарот", +"Save" => "Сними", +"Submit" => "Прати", +"Cancel" => "Откажи", +"Edit an event" => "Уреди настан", +"Export" => "Извези", +"Eventinfo" => "Инфо за настан", +"Repeating" => "Повторување", +"Alarm" => "Аларм", +"Attendees" => "Присутни", +"Share" => "Сподели", +"Title of the Event" => "Наслов на настанот", +"Category" => "Категорија", +"Separate categories with commas" => "Одвоете ги категориите со запирка", +"Edit categories" => "Уреди категории", +"All Day Event" => "Целодневен настан", +"From" => "Од", +"To" => "До", +"Advanced options" => "Напредни опции", +"Location" => "Локација", +"Location of the Event" => "Локација на настанот", +"Description" => "Опис", +"Description of the Event" => "Опис на настанот", +"Repeat" => "Повтори", +"Advanced" => "Напредно", +"Select weekdays" => "Избери работни денови", +"Select days" => "Избери денови", +"and the events day of year." => "и настаните ден од година.", +"and the events day of month." => "и настаните ден од месец.", +"Select months" => "Избери месеци", +"Select weeks" => "Избери седмици", +"and the events week of year." => "и настаните седмица од година.", +"Interval" => "интервал", +"End" => "Крај", +"occurrences" => "повторувања", +"create a new calendar" => "создади нов календар", +"Import a calendar file" => "Внеси календар од датотека ", +"Please choose a calendar" => "Ве молам изберете календар", +"Name of new calendar" => "Име на новиот календар", +"Take an available name!" => "Земи достапно име!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Календар со ова име веќе постои. Ако сепак продолжите календарите ќе бидат споени.", +"Remove all events from the selected calendar" => "Избришете ги сите настани од избраниот календар", +"Import" => "Увези", +"Close Dialog" => "Затвори дијалог", +"Create a new event" => "Создади нов настан", +"Share with:" => "Сподели со:", +"Shared with" => "Споделено со", +"Unshare" => "Не споделувај", +"Nobody" => "Никој", +"Shared via calendar" => "Сподели преку календар", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "Забелешка: Акциите на настани споделени преку календар ќе влијаат на сето споделување на календар.", +"View an event" => "Погледај настан", +"No categories selected" => "Нема избрано категории", +"of" => "од", +"at" => "на", +"General" => "Општо", +"Timezone" => "Временска зона", +"Update timezone automatically" => "Автоматски ажурирај временска зона", +"Time format" => "Формат на време", +"24h" => "24ч", +"12h" => "12ч", +"Start week on" => "Почни седмица со", +"Cache" => "Кеш", +"Clear cache for repeating events" => "Избриши го кешот за повторувачки настани", +"URLs" => "URL", +"Calendar CalDAV syncing addresses" => "CalDAV адреси за синхронизирање со календар", +"more info" => "повеќе информации", +"Primary address (Kontact et al)" => "Примарна адреса", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "iCalenar врска(и) само за читање" +); diff --git a/l10n/ms_MY.php b/l10n/ms_MY.php new file mode 100644 index 000000000..221f518d2 --- /dev/null +++ b/l10n/ms_MY.php @@ -0,0 +1,172 @@ + "Tiada kalendar dijumpai.", +"No events found." => "Tiada agenda dijumpai.", +"Wrong calendar" => "Silap kalendar", +"New Timezone:" => "Timezone Baru", +"Timezone changed" => "Zon waktu diubah", +"Invalid request" => "Permintaan tidak sah", +"Calendar" => "Kalendar", +"Deletion failed" => "Pemadaman gagal", +"Editable" => "Boleh disunting", +"ddd" => "ddd", +"ddd M/d" => "dd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyy", +"Sunday" => "Ahad", +"Monday" => "Isnin", +"Tuesday" => "Selasa", +"Wednesday" => "Rabu", +"Thursday" => "Khamis", +"Friday" => "Jumaat", +"Saturday" => "Sabtu", +"Sun." => "Ahad", +"Mon." => "Isnin", +"Tue." => "Selasa", +"Wed." => "Rabu ", +"Thu." => "Khamis", +"Fri." => "Jumaat", +"Sat." => "Sabtu", +"January" => "Januari", +"February" => "Februari", +"March" => "Mac", +"April" => "April", +"May" => "Mei", +"June" => "Jun", +"July" => "Julai", +"August" => "Ogos", +"September" => "September", +"October" => "Oktober", +"November" => "November", +"December" => "Disember", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mac.", +"Apr." => "Apr.", +"May." => "May.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Ogos.", +"Sep." => "Sept.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dis.", +"All day" => "Sepanjang hari", +"New Calendar" => "Kalendar baru", +"Title" => "Tajuk", +"From Date" => "Dari tarikh", +"From Time" => "Masa Dari", +"To Date" => "Sehingga kini", +"To Time" => "Semasa", +"The event ends before it starts" => "Peristiwa berakhir sebelum bermula", +"There was a database fail" => "Terdapat kegagalan pada pengkalan data", +"Birthday" => "Hari lahir", +"Business" => "Perniagaan", +"Call" => "Panggilan", +"Clients" => "Klien", +"Deliverer" => "Penghantar", +"Holidays" => "Cuti", +"Ideas" => "Idea", +"Journey" => "Perjalanan", +"Jubilee" => "Jubli", +"Meeting" => "Perjumpaan", +"Other" => "Lain", +"Personal" => "Peribadi", +"Projects" => "Projek", +"Questions" => "Soalan", +"Work" => "Kerja", +"unnamed" => "tiada nama", +"Does not repeat" => "Tidak berulang", +"Daily" => "Harian", +"Weekly" => "Mingguan", +"Every Weekday" => "Setiap hari minggu", +"Bi-Weekly" => "Dua kali seminggu", +"Monthly" => "Bulanan", +"Yearly" => "Tahunan", +"never" => "jangan", +"by occurrences" => "dari kekerapan", +"by date" => "dari tarikh", +"by monthday" => "dari haribulan", +"by weekday" => "dari hari minggu", +"events week of month" => "event minggu dari bulan", +"first" => "pertama", +"second" => "kedua", +"third" => "ketiga", +"fourth" => "keempat", +"fifth" => "kelima", +"last" => "akhir", +"by events date" => "dari tarikh event", +"by yearday(s)" => "dari tahun", +"by weeknumber(s)" => "dari nombor minggu", +"by day and month" => "dari hari dan bulan", +"Date" => "Tarikh", +"Cal." => "Kalendar", +"Week" => "Minggu", +"Month" => "Bulan", +"List" => "Senarai", +"Today" => "Hari ini", +"Settings" => "Tetapan", +"Your calendars" => "Kalendar anda", +"CalDav Link" => "Pautan CalDav", +"Share Calendar" => "Kongsi Kalendar", +"Download" => "Muat turun", +"Edit" => "Edit", +"Delete" => "Hapus", +"New calendar" => "Kalendar baru", +"Edit calendar" => "Edit kalendar", +"Displayname" => "Paparan nama", +"Active" => "Aktif", +"Calendar color" => "Warna kalendar", +"Save" => "Simpan", +"Submit" => "Hantar", +"Cancel" => "Batal", +"Edit an event" => "Edit agenda", +"Export" => "Export", +"Eventinfo" => "Maklumat agenda", +"Repeating" => "Pengulangan", +"Alarm" => "Penggera", +"Attendees" => "Jemputan", +"Share" => "Berkongsi", +"Title of the Event" => "Tajuk agenda", +"Category" => "kategori", +"Separate categories with commas" => "Asingkan kategori dengan koma", +"Edit categories" => "Sunting Kategori", +"All Day Event" => "Agenda di sepanjang hari ", +"From" => "Dari", +"To" => "ke", +"Advanced options" => "Pilihan maju", +"Location" => "Lokasi", +"Location of the Event" => "Lokasi agenda", +"Description" => "Huraian", +"Description of the Event" => "Huraian agenda", +"Repeat" => "Ulang", +"Advanced" => "Maju", +"Select weekdays" => "Pilih hari minggu", +"Select days" => "Pilih hari", +"and the events day of year." => "dan hari event dalam tahun.", +"and the events day of month." => "dan hari event dalam bulan.", +"Select months" => "Pilih bulan", +"Select weeks" => "Pilih minggu", +"and the events week of year." => "dan event mingguan dalam setahun.", +"Interval" => "Tempoh", +"End" => "Tamat", +"occurrences" => "Peristiwa", +"create a new calendar" => "Cipta kalendar baru", +"Import a calendar file" => "Import fail kalendar", +"Name of new calendar" => "Nama kalendar baru", +"Import" => "Import", +"Close Dialog" => "Tutup dialog", +"Create a new event" => "Buat agenda baru", +"View an event" => "Papar peristiwa", +"No categories selected" => "Tiada kategori dipilih", +"of" => "dari", +"at" => "di", +"General" => "Umum", +"Timezone" => "Zon waktu", +"24h" => "24h", +"12h" => "12h", +"more info" => "maklumat lanjut", +"Primary address (Kontact et al)" => "Alamat utama", +"iOS/OS X" => "iOS/OS X" +); diff --git a/l10n/my_MM.php b/l10n/my_MM.php new file mode 100644 index 000000000..c1c7d01b7 --- /dev/null +++ b/l10n/my_MM.php @@ -0,0 +1,38 @@ + "အချိန်ဇုံပြောင်းလဲသည်", +"Invalid request" => "တောင်းဆိုချက်မမှန်ကန်ပါ", +"Calendar" => "ပြက္ခဒိန်", +"January" => "ဇန်နဝါရီ", +"February" => "ဖေဖော်ဝါရီ", +"March" => "မတ်", +"April" => "ဧပြီ", +"May" => "မေ", +"June" => "ဇွန်", +"July" => "ဇူလိုင်", +"August" => "ဩဂုတ်", +"September" => "စက်တင်ဘာ", +"October" => "အောက်တိုဘာ", +"November" => "နိုဝင်ဘာ", +"December" => "ဒီဇင်ဘာ", +"All day" => "နေ့တိုင်း", +"Title" => "ခေါင်းစဉ်", +"Daily" => "နေ့တိုင်း", +"Weekly" => "အပတ်တိုင်း", +"Week" => "အပတ်", +"Month" => "လ", +"Today" => "ယနေ့", +"Download" => "ဒေါင်းလုတ်", +"Edit calendar" => "ပြက္ခဒိန်ကိုပြင်မည်", +"Calendar color" => "ပြက္ခဒိန်အရောင်", +"Submit" => "ထည့်သွင်းမည်", +"Cancel" => "ပယ်ဖျက်မည်", +"Edit an event" => "ပွဲကိုပြင်ဆင်မည်", +"Title of the Event" => "ပွဲ၏ခေါင်းစဉ်", +"From" => "မှ", +"To" => "သို့", +"Location" => "တည်နေရာ", +"Location of the Event" => "ပွဲ၏တည်နေရာ", +"Description" => "ဖော်ပြချက်", +"Advanced" => "အဆင့်မြင့်", +"Create a new event" => "ပွဲအသစ်တစ်ပွဲပြုလုပ်မည်" +); diff --git a/l10n/nb_NO.php b/l10n/nb_NO.php new file mode 100644 index 000000000..e265b4c5a --- /dev/null +++ b/l10n/nb_NO.php @@ -0,0 +1,183 @@ + "Ingen kalendere funnet", +"No events found." => "Ingen hendelser funnet", +"Wrong calendar" => "Feil kalender", +"You do not have the permissions to edit this event." => "Du har ikke tilgang til å endre dette eventet", +"New Timezone:" => "Ny tidssone:", +"Timezone changed" => "Tidssone endret", +"Invalid request" => "Ugyldig forespørsel", +"Calendar" => "Kalender", +"Deletion failed" => "Sletting feilet", +"user" => "bruker", +"group" => "gruppe", +"Editable" => "Redigerbar", +"Sunday" => "Søndag", +"Monday" => "Mandag", +"Tuesday" => "Tirsdag", +"Wednesday" => "Onsdag", +"Thursday" => "Torsdag", +"Friday" => "Fredag", +"Saturday" => "Lørdag", +"Sun." => "Sø.", +"Mon." => "Ma.", +"Tue." => "Ti.", +"Wed." => "On.", +"Thu." => "To.", +"Fri." => "Fr.", +"Sat." => "Lø.", +"January" => "Januar", +"February" => "Februar", +"March" => "Mars", +"April" => "April", +"May" => "Mai", +"June" => "Juni", +"July" => "Juli", +"August" => "August", +"September" => "September", +"October" => "Oktober", +"November" => "November", +"December" => "Desember", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Apr.", +"May." => "Mai.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Des.", +"All day" => "Hele dagen ", +"New Calendar" => "Ny kalender", +"Title" => "Tittel", +"From Date" => "Fra dato", +"From Time" => "Fra tidspunkt", +"To Date" => "Til dato", +"To Time" => "Til tidspunkt", +"The event ends before it starts" => "En hendelse kan ikke slutte før den har begynt.", +"There was a database fail" => "Det oppstod en databasefeil.", +"Birthday" => "Bursdag", +"Business" => "Forretninger", +"Call" => "Ring", +"Clients" => "Kunder", +"Holidays" => "Ferie", +"Ideas" => "Ideér", +"Journey" => "Reise", +"Jubilee" => "Jubileum", +"Meeting" => "Møte", +"Other" => "Annet", +"Personal" => "ersonlig", +"Projects" => "Prosjekter", +"Questions" => "Spørsmål", +"Work" => "Arbeid", +"unnamed" => "uten navn", +"You do not have the permissions to update this calendar." => "Du har ikke tilgang til å oppdatere denne kalenderen", +"You do not have the permissions to delete this calendar." => "Du har ikke tilgang til å slette denne kalenderen", +"You do not have the permissions to add to this calendar." => "Du har ikke tilgang til å legge til noe i denne kalenderen", +"You do not have the permissions to delete this event." => "Du har ikke tilgang til å slette dette eventet", +"Does not repeat" => "Gjentas ikke", +"Daily" => "Daglig", +"Weekly" => "Ukentlig", +"Every Weekday" => "Hver ukedag", +"Bi-Weekly" => "Annenhver uke", +"Monthly" => "Månedlig", +"Yearly" => "Årlig", +"never" => "aldri", +"by occurrences" => "etter hyppighet", +"by date" => "etter dato", +"by monthday" => "etter dag i måned", +"by weekday" => "etter ukedag", +"events week of month" => "begivenhetens uke denne måneden", +"first" => "første", +"second" => "andre", +"third" => "tredje", +"fourth" => "fjerde", +"fifth" => "femte", +"last" => "siste", +"by events date" => "etter hendelsenes dato", +"by yearday(s)" => "etter dag i året", +"by weeknumber(s)" => "etter ukenummer/-numre", +"by day and month" => "etter dag og måned", +"Date" => "Dato", +"Cal." => "Kal.", +"Week" => "Uke", +"Month" => "ned", +"List" => "Liste", +"Today" => "I dag", +"Settings" => "Innstillinger", +"Your calendars" => "Dine kalendere", +"CalDav Link" => "CalDav-lenke", +"Share Calendar" => "Del Kalender", +"Download" => "Last ned", +"Edit" => "Endre", +"Delete" => "Slett", +"New calendar" => "Ny kalender", +"Edit calendar" => "Rediger kalender", +"Displayname" => "Visningsnavn", +"Active" => "Aktiv", +"Calendar color" => "Kalenderfarge", +"Save" => "Lagre", +"Submit" => "Lagre", +"Cancel" => "Avbryt", +"Edit an event" => "Rediger en hendelse", +"Export" => "Eksporter", +"Eventinfo" => "Hendelsesinformasjon", +"Repeating" => "Gjentas", +"Alarm" => "Alarm", +"Attendees" => "Deltakere", +"Share" => "Del", +"Title of the Event" => "Hendelsestittel", +"Category" => "Kategori", +"Separate categories with commas" => "Separer kategorier med komma", +"Edit categories" => "Rediger kategorier", +"All Day Event" => "Hele dagen-hendelse", +"From" => "Fra", +"To" => "Til", +"Advanced options" => "Avanserte innstillinger", +"Location" => "Sted", +"Location of the Event" => "Hendelsessted", +"Description" => "Beskrivelse", +"Description of the Event" => "Hendelesebeskrivelse", +"Repeat" => "Gjenta", +"Advanced" => "Avansert", +"Select weekdays" => "Velg ukedager", +"Select days" => "Velg dager", +"and the events day of year." => "og hendelsenes dag i året.", +"and the events day of month." => "og hendelsenes dag i måneden.", +"Select months" => "Velg måneder", +"Select weeks" => "Velg uker", +"and the events week of year." => "og hendelsenes uke i året.", +"Interval" => "Intervall", +"End" => "Slutt", +"occurrences" => "forekomster", +"create a new calendar" => "Lag en ny kalender", +"Import a calendar file" => "Importer en kalenderfil", +"Please choose a calendar" => "Vennligst velg en kalender", +"Name of new calendar" => "Navn på ny kalender:", +"Take an available name!" => "Velg at tilgjengelig navn!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "En kalender med dette navnet finnes allerede. Hvis du velger å fortsette vil disse kalenderene bli slått sammen.", +"Remove all events from the selected calendar" => "Fjern alle events fra valgt kalender", +"Import" => "Importer", +"Close Dialog" => "Lukk dialog", +"Create a new event" => "Opprett en ny hendelse", +"Share with:" => "Del med:", +"Shared with" => "Delt med", +"Unshare" => "Avslutt deling", +"Nobody" => "Ingen", +"Shared via calendar" => "Delt via kalender", +"View an event" => "Se på hendelse", +"No categories selected" => "Ingen kategorier valgt", +"General" => "Generellt", +"Timezone" => "Tidssone", +"Update timezone automatically" => "Oppdater tidssone automatisk", +"24h" => "24 t", +"12h" => "12 t", +"Start week on" => "Start uke på", +"Cache" => "Buffer", +"URLs" => "URLs", +"more info" => "mer info", +"Primary address (Kontact et al)" => "Primær adresse (kontakt osv)", +"iOS/OS X" => "iOS/OS X" +); diff --git a/l10n/nl.php b/l10n/nl.php new file mode 100644 index 000000000..6a37cd187 --- /dev/null +++ b/l10n/nl.php @@ -0,0 +1,215 @@ + "Niet alle agenda's zijn volledig gecached", +"Everything seems to be completely cached" => "Alles lijkt volledig gecached te zijn", +"No calendars found." => "Geen agenda's gevonden.", +"No events found." => "Geen gebeurtenissen gevonden.", +"Wrong calendar" => "Verkeerde agenda", +"You do not have the permissions to edit this event." => "U heeft geen permissie om deze gebeurtenis te bewerken.", +"The file contained either no events or all events are already saved in your calendar." => "Het bestand bevat geen gebeurtenissen of alle gebeurtenissen worden al in uw agenda bewaard.", +"events has been saved in the new calendar" => "De gebeurtenissen worden in de nieuwe agenda bewaard", +"Import failed" => "import is gefaald", +"events has been saved in your calendar" => "de gebeurtenissen zijn in uw agenda opgeslagen ", +"New Timezone:" => "Nieuwe tijdszone:", +"Timezone changed" => "Tijdzone is veranderd", +"Invalid request" => "Ongeldige aanvraag", +"Calendar" => "Agenda", +"Deletion failed" => "Verwijderen mislukt", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "gebruiker", +"group" => "groep", +"Editable" => "Te wijzigen", +"Shareable" => "Te delen", +"Deletable" => "Verwijderbaar", +"ddd" => "ddd", +"ddd M/d" => "ddd d.M", +"dddd M/d" => "dddd d.M", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "d[ MMM][ yyyy]{ '—' d MMM yyyy}", +"dddd, MMM d, yyyy" => "dddd, d. MMM yyyy", +"Sunday" => "Zondag", +"Monday" => "Maandag", +"Tuesday" => "Dinsdag", +"Wednesday" => "Woensdag", +"Thursday" => "Donderdag", +"Friday" => "Vrijdag", +"Saturday" => "Zaterdag", +"Sun." => "Zon.", +"Mon." => "Maa.", +"Tue." => "Din.", +"Wed." => "Woe.", +"Thu." => "Don.", +"Fri." => "Vrij.", +"Sat." => "Zat.", +"January" => "Januari", +"February" => "Februari", +"March" => "Maart", +"April" => "April", +"May" => "Mei", +"June" => "Juni", +"July" => "Juli", +"August" => "Augustus", +"September" => "September", +"October" => "Oktober", +"November" => "November", +"December" => "December", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Maa.", +"Apr." => "Apr.", +"May." => "Mei.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dec.", +"All day" => "Hele dag", +"New Calendar" => "Nieuwe agenda", +"Missing or invalid fields" => "Missende of ongeldige velden", +"Title" => "Titel", +"From Date" => "Begindatum", +"From Time" => "Begintijd", +"To Date" => "Einddatum", +"To Time" => "Eindtijd", +"The event ends before it starts" => "De gebeurtenis eindigt voor het begin", +"There was a database fail" => "Er was een databasefout", +"Birthday" => "Verjaardag", +"Business" => "Zakelijk", +"Call" => "Bellen", +"Clients" => "Klanten", +"Deliverer" => "Leverancier", +"Holidays" => "Vakantie", +"Ideas" => "Ideeën", +"Journey" => "Reis", +"Jubilee" => "Jubileum", +"Meeting" => "Vergadering", +"Other" => "Ander", +"Personal" => "Persoonlijk", +"Projects" => "Projecten", +"Questions" => "Vragen", +"Work" => "Werk", +"by" => "door", +"unnamed" => "onbekend", +"You do not have the permissions to update this calendar." => "U heeft geen permissie om deze agenda te bewerken.", +"You do not have the permissions to delete this calendar." => "U heeft geen permissie om deze agenda te verwijderen.", +"You do not have the permissions to add to this calendar." => "U heeft geen permissie om deze agenda toe te voegen.", +"You do not have the permissions to add events to this calendar." => "U heeft geen permissie om gebeurtenissen aan deze agenda toe te voegen.", +"You do not have the permissions to delete this event." => "U heeft geen permissie om deze gebeurtenis te verwijderen.", +"Busy" => "Bezig", +"Public" => "Openbaar", +"Private" => "Privé", +"Confidential" => "Vertrouwelijk", +"Does not repeat" => "Geen", +"Daily" => "Dagelijks", +"Weekly" => "Wekelijks", +"Every Weekday" => "Elke weekdag", +"Bi-Weekly" => "Tweewekelijks", +"Monthly" => "Maandelijks", +"Yearly" => "Jaarlijks", +"never" => "geen", +"by occurrences" => "volgens gebeurtenissen", +"by date" => "op datum", +"by monthday" => "per dag van de maand", +"by weekday" => "op weekdag", +"events week of month" => "gebeurtenissen week van maand", +"first" => "eerste", +"second" => "tweede", +"third" => "derde", +"fourth" => "vierde", +"fifth" => "vijfde", +"last" => "laatste", +"by events date" => "volgens gebeurtenisdatum", +"by yearday(s)" => "volgens jaardag(en)", +"by weeknumber(s)" => "volgens weeknummer(s)", +"by day and month" => "per dag en maand", +"Date" => "Datum", +"Cal." => "Cal.", +"Week" => "Week", +"Month" => "Maand", +"List" => "Lijst", +"Today" => "Vandaag", +"Settings" => "Instellingen", +"Your calendars" => "Uw agenda's", +"CalDav Link" => "CalDav Link", +"Share Calendar" => "Deel kalender", +"Download" => "Download", +"Edit" => "Bewerken", +"Delete" => "Verwijderen", +"New calendar" => "Nieuwe agenda", +"Edit calendar" => "Bewerk agenda", +"Displayname" => "Weergavenaam", +"Active" => "Actief", +"Calendar color" => "Agenda kleur", +"Save" => "Opslaan", +"Submit" => "Opslaan", +"Cancel" => "Annuleren", +"Edit an event" => "Bewerken van een afspraak", +"Export" => "Exporteren", +"Eventinfo" => "Gebeurtenisinformatie", +"Repeating" => "Herhaling", +"Alarm" => "Alarm", +"Attendees" => "Deelnemers", +"Share" => "Delen", +"Title of the Event" => "Titel van de afspraak", +"Category" => "Categorie", +"Separate categories with commas" => "Gescheiden door komma's", +"Edit categories" => "Wijzig categorieën", +"Access Class" => "Toegangsklasse", +"All Day Event" => "Hele dag", +"From" => "Van", +"To" => "Aan", +"Advanced options" => "Geavanceerde opties", +"Location" => "Locatie", +"Location of the Event" => "Locatie van de afspraak", +"Description" => "Beschrijving", +"Description of the Event" => "Beschrijving van de gebeurtenis", +"Repeat" => "Herhaling", +"Advanced" => "Geavanceerd", +"Select weekdays" => "Selecteer weekdagen", +"Select days" => "Selecteer dagen", +"and the events day of year." => "en de gebeurtenissen dag van het jaar", +"and the events day of month." => "en de gebeurtenissen dag van de maand", +"Select months" => "Selecteer maanden", +"Select weeks" => "Selecteer weken", +"and the events week of year." => "en de gebeurtenissen week van het jaar", +"Interval" => "Interval", +"End" => "Einde", +"occurrences" => "gebeurtenissen", +"create a new calendar" => "Maak een nieuw agenda", +"Import a calendar file" => "Importeer een agenda bestand", +"Please choose a calendar" => "Kies een agenda", +"Name of new calendar" => "Naam van de nieuwe agenda", +"Take an available name!" => "Kies een beschikbare naam!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Een agenda met deze naam bestaat al. Als u doorgaat, worden deze agenda's samengevoegd", +"Remove all events from the selected calendar" => "Verwijder alle gebeurtenissen van de geselecteerde agenda", +"Import" => "Importeer", +"Close Dialog" => "Sluit venster", +"Create a new event" => "Maak een nieuwe afspraak", +"Share with:" => "Deel met:", +"Shared with" => "Gedeeld met", +"Unshare" => "Stop met delen", +"Nobody" => "Niemand", +"Shared via calendar" => "Gedeeld via agenda", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "Let op: acties op gebeurtenissen die gedeeld zijn via een agenda, hebben effect op de gehele agenda deling", +"View an event" => "Bekijk een gebeurtenis", +"No categories selected" => "Geen categorieën geselecteerd", +"of" => "van", +"at" => "op", +"General" => "Algemeen", +"Timezone" => "Tijdzone", +"Update timezone automatically" => "Werk de tijdzone automatisch bij", +"Time format" => "Tijd formaat", +"24h" => "24uur", +"12h" => "12uur", +"Start week on" => "Begin de week op", +"Cache" => "Cache", +"Clear cache for repeating events" => "Leeg cache voor repeterende gebeurtenissen", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Agenda CalDAV synchronisatie adres", +"more info" => "meer informatie", +"Primary address (Kontact et al)" => "Primary adres (voor Kontact en dergelijke)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Alleen lezen iCalendar link(en)" +); diff --git a/l10n/nn_NO.php b/l10n/nn_NO.php new file mode 100644 index 000000000..011347021 --- /dev/null +++ b/l10n/nn_NO.php @@ -0,0 +1,144 @@ + "Feil kalender", +"New Timezone:" => "Ny tidssone:", +"Timezone changed" => "Endra tidssone", +"Invalid request" => "Ugyldig førespurnad", +"Calendar" => "Kalender", +"Sunday" => "Søndag", +"Monday" => "Måndag", +"Tuesday" => "Tysdag", +"Wednesday" => "Onsdag", +"Thursday" => "Torsdag", +"Friday" => "Fredag", +"Saturday" => "Laurdag", +"Sun." => "Søn.", +"Mon." => "Mån.", +"Tue." => "Tys.", +"Wed." => "Ons.", +"Thu." => "Tor.", +"Fri." => "Fre.", +"Sat." => "Lau.", +"January" => "Januar", +"February" => "Februar", +"March" => "Mars", +"April" => "April", +"May" => "Mai", +"June" => "Juni", +"July" => "Juli", +"August" => "August", +"September" => "September", +"October" => "Oktober", +"November" => "November", +"December" => "Desember", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mar,", +"Apr." => "Apr.", +"May." => "Mai.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Des.", +"All day" => "Heile dagen", +"New Calendar" => "Ny kalender", +"Title" => "Tittel", +"From Date" => "Frå dato", +"From Time" => "Frå tid", +"To Date" => "Til dato", +"To Time" => "Til tid", +"The event ends before it starts" => "Hendinga endar før den startar", +"There was a database fail" => "Det oppstod ein databasefeil", +"Birthday" => "Bursdag", +"Business" => "Forretning", +"Call" => "Telefonsamtale", +"Clients" => "Klientar", +"Deliverer" => "Forsending", +"Holidays" => "Høgtid", +"Ideas" => "Idear", +"Journey" => "Reise", +"Jubilee" => "Jubileum", +"Meeting" => "Møte", +"Other" => "Anna", +"Personal" => "Personleg", +"Projects" => "Prosjekt", +"Questions" => "Spørsmål", +"Work" => "Arbeid", +"Does not repeat" => "Ikkje gjenta", +"Daily" => "Kvar dag", +"Weekly" => "Kvar veke", +"Every Weekday" => "Kvar vekedag", +"Bi-Weekly" => "Annakvar veke", +"Monthly" => "Kvar månad", +"Yearly" => "Kvart år", +"never" => "aldri", +"by occurrences" => "av førekomstar", +"by date" => "av dato", +"by monthday" => "av månadsdag", +"by weekday" => "av vekedag", +"events week of month" => "hendingas veke av månad", +"first" => "første", +"second" => "andre", +"third" => "tredje", +"fourth" => "fjerde", +"fifth" => "femte", +"last" => "siste", +"by events date" => "av hendingsdato", +"by yearday(s)" => "av årsdag(ar)", +"by weeknumber(s)" => "av vekenummer", +"by day and month" => "av dag og månad", +"Date" => "Dato", +"Cal." => "Kal.", +"Week" => "Veke", +"Month" => "Månad", +"List" => "Liste", +"Today" => "I dag", +"Settings" => "Innstillingar", +"CalDav Link" => "CalDav-lenkje", +"Download" => "Last ned", +"Edit" => "Endra", +"Delete" => "Slett", +"New calendar" => "Ny kalender", +"Edit calendar" => "Endra kalendarar", +"Displayname" => "Visingsnamn", +"Active" => "Aktiv", +"Calendar color" => "Kalenderfarge", +"Save" => "Lagra", +"Submit" => "Lagra", +"Cancel" => "Avbryt", +"Edit an event" => "Endra ein hending", +"Export" => "Eksporter", +"Title of the Event" => "Tittel på hendinga", +"Category" => "Kategori", +"All Day Event" => "Heildagshending", +"From" => "Frå", +"To" => "Til", +"Advanced options" => "Avanserte alternativ", +"Location" => "Stad", +"Location of the Event" => "Stad for hendinga", +"Description" => "Skildring", +"Description of the Event" => "Skildring av hendinga", +"Repeat" => "Gjenta", +"Advanced" => "Avansert", +"Select weekdays" => "Vel vekedagar", +"Select days" => "Vel dagar", +"and the events day of year." => "og hendingane dag for år.", +"and the events day of month." => "og hendingane dag for månad.", +"Select months" => "Vel månedar", +"Select weeks" => "Vel veker", +"and the events week of year." => "og hendingane veke av året.", +"Interval" => "Intervall", +"End" => "Ende", +"occurrences" => "førekomstar", +"create a new calendar" => "Lag ny kalender", +"Import a calendar file" => "Importer ei kalenderfil", +"Name of new calendar" => "Namn for ny kalender", +"Import" => "Importer", +"Close Dialog" => "Steng dialog", +"Create a new event" => "Opprett ei ny hending", +"Timezone" => "Tidssone", +"24h" => "24t", +"12h" => "12t" +); diff --git a/l10n/oc.php b/l10n/oc.php new file mode 100644 index 000000000..b88d8c04d --- /dev/null +++ b/l10n/oc.php @@ -0,0 +1,206 @@ + "Los calendièrs son pas completament en cache", +"Everything seems to be completely cached" => "Tot sembla completament en cache", +"No calendars found." => "Cap de calendièr trobat.", +"No events found." => "Cap d'eveniment trobat.", +"Wrong calendar" => "Calendièr pas corrècte", +"You do not have the permissions to edit this event." => "Sias pas permés d'editar aqueste eveniment.", +"The file contained either no events or all events are already saved in your calendar." => "Lo fichièr contenguèt siá pas d'eveniments o totes los eveniments son ja enregistrats dins ton calendièr.", +"events has been saved in the new calendar" => "eveniments que son estats enregistrats dins un calendièr novèl", +"Import failed" => "Fracàs d'importacion", +"events has been saved in your calendar" => "eveniments que son estats enregistrats dins lo calendièr teu", +"New Timezone:" => "Zona novèla :", +"Timezone changed" => "Zona cambiada", +"Invalid request" => "Demanda invalida", +"Calendar" => "Calendièr", +"Deletion failed" => "Fracàs d'escafatge", +"user" => "usancièr", +"group" => "grop", +"Editable" => "Editable", +"Shareable" => "Partejador", +"Deletable" => "Escafable", +"ddd" => "jjj", +"ddd M/d" => "jjj M/j", +"dddd M/d" => "jjjj M/j", +"MMMM yyyy" => "MMMM aaaa", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM j[ aaaa]{ '—'[ MMM] j aaaa}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Dimenge", +"Monday" => "Diluns", +"Tuesday" => "Dimarç", +"Wednesday" => "Dimecres", +"Thursday" => "Dijòus", +"Friday" => "Divendres", +"Saturday" => "Dissabte", +"Sun." => "Dim.", +"Mon." => "Luns.", +"Tue." => "Març.", +"Wed." => "Mec.", +"Thu." => "Jòu.", +"Fri." => "Ven.", +"Sat." => "Sab.", +"January" => "genièr", +"February" => "febrièr", +"March" => "març", +"April" => "abril", +"May" => "mai", +"June" => "junh", +"July" => "julhet", +"August" => "agost", +"September" => "septembre", +"October" => "octobre", +"November" => "Novembre", +"December" => "Decembre", +"Jan." => "Gen.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Abr.", +"May." => "Mai.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Ago.", +"Sep." => "Sep.", +"Oct." => "Oct.", +"Nov." => "Nov.", +"Dec." => "Dec.", +"All day" => "Totes los jorns", +"New Calendar" => "Calendièr novèl", +"Title" => "Títol", +"From Date" => "Dempuèi (data)", +"From Time" => "Dempuèi (ora) ", +"To Date" => "Cap a ~data~", +"To Time" => "Cap a ~ora~", +"The event ends before it starts" => "L'eveniment s'acaba avans sa debuta", +"There was a database fail" => "I a agut un fracàs de basa de donadas", +"Birthday" => "Anniversari", +"Business" => "Afar", +"Call" => "Crida", +"Clients" => "Practica", +"Deliverer" => "Liurason", +"Holidays" => "Vacanças", +"Ideas" => "Idèas", +"Journey" => "Viatge", +"Jubilee" => "Jubileu", +"Meeting" => "Encontra", +"Other" => "Autres", +"Personal" => "Personal", +"Projects" => "Projèctes", +"Questions" => "Questions", +"Work" => "Trabalh", +"by" => "per", +"unnamed" => "pas nomenat", +"You do not have the permissions to update this calendar." => "Sias pas permés de metre a jorn aqueste calendièr.", +"You do not have the permissions to delete this calendar." => "Sias pas permés d'escafar aqueste calendièr.", +"You do not have the permissions to add to this calendar." => "Sias pas permés d'apondre a 'n aqueste calendièr.", +"You do not have the permissions to add events to this calendar." => "Sias pas permés d'apondre eveniments a 'n aqueste calendièr.", +"You do not have the permissions to delete this event." => "Sias pas permés d'escafar aqueste eveniment.", +"Does not repeat" => "Torna pas far", +"Daily" => "Jornalièr", +"Weekly" => "Setmanièr", +"Every Weekday" => "Cada jorn de la setmana", +"Bi-Weekly" => "Dos setmanièr", +"Monthly" => "Mesadièr", +"Yearly" => "Annadièr", +"never" => "jamai", +"by occurrences" => "per ocurenças", +"by date" => "per data", +"by monthday" => "per jorn del mes", +"by weekday" => "per jorn de setmana", +"events week of month" => "eveniments setmanièrs del mes", +"first" => "primièr", +"second" => "second", +"third" => "tresen", +"fourth" => "quatren", +"fifth" => "cinquen", +"last" => "darrièr", +"by events date" => "per data d'eveniments", +"by yearday(s)" => "per jorn(s) d'annada", +"by weeknumber(s)" => "per numero(s) de setmana", +"by day and month" => "per jorn e mes", +"Date" => "Data", +"Cal." => "Cal.", +"Week" => "Setmana", +"Month" => "Mes", +"List" => "Tièra", +"Today" => "Uèi", +"Settings" => "Configuracion", +"Your calendars" => "Los calendièrs vòstres", +"CalDav Link" => "Ligam CalDav", +"Share Calendar" => "Parteja lo calendièr", +"Download" => "Avalcargar", +"Edit" => "Editar", +"Delete" => "Escafa", +"New calendar" => "Calendièr novèl", +"Edit calendar" => "Edita calendièr", +"Displayname" => "Nom afichat", +"Active" => "Actiu", +"Calendar color" => "Color de calendièr", +"Save" => "Enregistra", +"Submit" => "Sosmetre", +"Cancel" => "Anulla", +"Edit an event" => "Edita un eveniment", +"Export" => "Exporta", +"Eventinfo" => "Eventinfo", +"Repeating" => "Al tornar far", +"Alarm" => "Alarma", +"Attendees" => "Convidats", +"Share" => "Parteja", +"Title of the Event" => "Títol del eveniment", +"Category" => "Categoria", +"Separate categories with commas" => "Destria categorias amb virgulas", +"Edit categories" => "Edita categorias", +"All Day Event" => "Eveniment de cada jorn", +"From" => "De", +"To" => "Per", +"Advanced options" => "Opcions avançadas", +"Location" => "Plaça", +"Location of the Event" => "Plaça del eveniment", +"Description" => "Descripcion", +"Description of the Event" => "Descripcion del eveniment", +"Repeat" => "Torna far", +"Advanced" => "Avançat", +"Select weekdays" => "Selecciona los jorns de setmana", +"Select days" => "Selecciona los jorns", +"and the events day of year." => "e los eveniments jornalièrs de l'annada.", +"and the events day of month." => "e los eveniments del mes.", +"Select months" => "Selecciona los meses", +"Select weeks" => "Selecciona las setmanas", +"and the events week of year." => "e los eveniments setmanièrs de l'annada.", +"Interval" => "Interval", +"End" => "Fin", +"occurrences" => "ocurréncias", +"create a new calendar" => "Crea un calendièr novèl", +"Import a calendar file" => "Importa un fichièr calendièr", +"Please choose a calendar" => "Causís un calendièr, se te plai", +"Name of new calendar" => "Nom del calendièr novèl", +"Take an available name!" => "Pren-te un nom disponible !", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Ja un calendièr amb lo meme nom existís. Se contunha aqueles seràn mesclats de tot biais.", +"Import" => "Importa", +"Close Dialog" => "Tampa lo dialòg", +"Create a new event" => "Crea un eveniment nòu", +"Share with:" => "Parteja amb :", +"Shared with" => "Partejat amb :", +"Unshare" => "Pas partejador", +"Nobody" => "degun", +"Shared via calendar" => "Partejat tras calendièr", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "AVÍS: las actions suls eveniments partejats tras calendièr cambiaràn los partatge de calendièr complet.", +"View an event" => "Espiar un eveniment", +"No categories selected" => "Cap de categorias seleccionadas", +"of" => "de", +"at" => "a", +"General" => "General", +"Timezone" => "Zona", +"Update timezone automatically" => "Zona mesa a jorn automaticament", +"Time format" => "Format de l'ora", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "La setmana comença lo", +"Cache" => "Cache", +"Clear cache for repeating events" => "Voida lo cache per los eveniments repetitius", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Calendar CalDAV syncing addresses", +"more info" => "mai d'entresenhes", +"Primary address (Kontact et al)" => "Adreiças primarias (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Ligam(s) iCalendar en lectura sola" +); diff --git a/l10n/pl.php b/l10n/pl.php new file mode 100644 index 000000000..5e26de059 --- /dev/null +++ b/l10n/pl.php @@ -0,0 +1,215 @@ + "Nie wszystkie kalendarze są całkowicie buforowane", +"Everything seems to be completely cached" => "Wszystko wydaje się być całkowicie buforowane", +"No calendars found." => "Nie znaleziono kalendarzy.", +"No events found." => "Nie znaleziono wydarzeń.", +"Wrong calendar" => "Nieprawidłowy kalendarz", +"You do not have the permissions to edit this event." => "Nie masz uprawnień, aby edytować to wydarzenie.", +"The file contained either no events or all events are already saved in your calendar." => "Plik nie zawierał żadnych wydarzeń lub wszystkie wydarzenia są już zapisane w kalendarzu.", +"events has been saved in the new calendar" => "zdarzenia zostały zapisane w nowym kalendarzu", +"Import failed" => "Importowanie nieudane", +"events has been saved in your calendar" => "wydarzenia zostały zapisane w twoim kalendarzu", +"New Timezone:" => "Nowa strefa czasowa:", +"Timezone changed" => "Zmieniono strefę czasową", +"Invalid request" => "Nieprawidłowe żądanie", +"Calendar" => "Kalendarz", +"Deletion failed" => "Usunięcie nie powiodło się", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ rrrr]{ - [ddd d] MMMM rrrr}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ rrrr] GG:mm{ - [ ddd d MMMM rrrr] GG:mm}", +"user" => "użytkownik", +"group" => "grupa", +"Editable" => "Edytowalne", +"Shareable" => "Możliwe do udostępniania", +"Deletable" => "Usuwalne", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Niedziela", +"Monday" => "Poniedziałek", +"Tuesday" => "Wtorek", +"Wednesday" => "Środa", +"Thursday" => "Czwartek", +"Friday" => "Piątek", +"Saturday" => "Sobota", +"Sun." => "N.", +"Mon." => "Pn.", +"Tue." => "Wt.", +"Wed." => "Śr.", +"Thu." => "Cz.", +"Fri." => "Pt.", +"Sat." => "S.", +"January" => "Styczeń", +"February" => "Luty", +"March" => "Marzec", +"April" => "Kwiecień", +"May" => "Maj", +"June" => "Czerwiec", +"July" => "Lipiec", +"August" => "Sierpień", +"September" => "Wrzesień", +"October" => "Październik", +"November" => "Listopad", +"December" => "Grudzień", +"Jan." => "Sty.", +"Feb." => "Lut.", +"Mar." => "Mar.", +"Apr." => "Kwi.", +"May." => "Maj.", +"Jun." => "Cze.", +"Jul." => "Lip.", +"Aug." => "Sie.", +"Sep." => "Wrz.", +"Oct." => "Paź.", +"Nov." => "Lis.", +"Dec." => "Gru.", +"All day" => "Cały dzień", +"New Calendar" => "Nowy kalendarz", +"Missing or invalid fields" => "Brak lub niewłaściwe pola", +"Title" => "Tytuł", +"From Date" => "Od daty", +"From Time" => "Od czasu", +"To Date" => "Do daty", +"To Time" => "Do czasu", +"The event ends before it starts" => "Wydarzenie kończy się przed rozpoczęciem", +"There was a database fail" => "Awaria bazy danych", +"Birthday" => "Urodziny", +"Business" => "Biznesowe", +"Call" => "Zadzwoń", +"Clients" => "Klienci", +"Deliverer" => "Dostawca", +"Holidays" => "Święta", +"Ideas" => "Pomysły", +"Journey" => "Podróż", +"Jubilee" => "Jubileusz", +"Meeting" => "Spotkanie", +"Other" => "Inne", +"Personal" => "Osobiste", +"Projects" => "Projekty", +"Questions" => "Pytania", +"Work" => "Zawodowe", +"by" => "przez", +"unnamed" => "nienazwany", +"You do not have the permissions to update this calendar." => "Nie masz uprawnień do aktualizacji tego kalendarza.", +"You do not have the permissions to delete this calendar." => "Nie masz uprawnień, aby usunąć ten kalendarz.", +"You do not have the permissions to add to this calendar." => "Nie masz uprawnień, aby dodawać do tego kalendarza.", +"You do not have the permissions to add events to this calendar." => "Nie masz uprawnień, aby dodać wydarzenia do tego kalendarza.", +"You do not have the permissions to delete this event." => "Nie masz uprawnień, aby usunąć to wydarzenie.", +"Busy" => "Zajęty", +"Public" => "Publiczne", +"Private" => "Prywatny", +"Confidential" => "Poufne", +"Does not repeat" => "Nie powtarza się", +"Daily" => "Codziennie", +"Weekly" => "Cotygodniowo", +"Every Weekday" => "Każdego dnia tygodnia", +"Bi-Weekly" => "Co dwa tygodnie", +"Monthly" => "Comiesięcznie", +"Yearly" => "Corocznie", +"never" => "nigdy", +"by occurrences" => "po wystąpieniach", +"by date" => "po dacie", +"by monthday" => "po dniu miesiąca", +"by weekday" => "po dniu tygodnia", +"events week of month" => "wydarzenia miesiąca", +"first" => "pierwszy", +"second" => "drugi", +"third" => "trzeci", +"fourth" => "czwarty", +"fifth" => "piąty", +"last" => "ostatni", +"by events date" => "po datach wydarzeń", +"by yearday(s)" => "po dniach roku", +"by weeknumber(s)" => "po tygodniach", +"by day and month" => "po dniu i miesiącu", +"Date" => "Data", +"Cal." => "Kal.", +"Week" => "Tydzień", +"Month" => "Miesiąc", +"List" => "Lista", +"Today" => "Dziś", +"Settings" => "Ustawienia", +"Your calendars" => "Twoje kalendarze", +"CalDav Link" => "Odnośnik CalDAV", +"Share Calendar" => "Współdziel kalendarz", +"Download" => "Pobierz", +"Edit" => "Edytuj", +"Delete" => "Usuń", +"New calendar" => "Nowy kalendarz", +"Edit calendar" => "Edytuj kalendarz", +"Displayname" => "Wyświetlana nazwa", +"Active" => "Aktywny", +"Calendar color" => "Kolor kalendarza", +"Save" => "Zapisz", +"Submit" => "Prześlij", +"Cancel" => "Anuluj", +"Edit an event" => "Edytuj wydarzenie", +"Export" => "Wyeksportuj", +"Eventinfo" => "Informacja o wydarzeniu", +"Repeating" => "Powtarzające się", +"Alarm" => "Alarm", +"Attendees" => "Uczestnicy", +"Share" => "Udostępnij", +"Title of the Event" => "Nazwa wydarzenia", +"Category" => "Kategoria", +"Separate categories with commas" => "Oddziel kategorie przecinkami", +"Edit categories" => "Edytuj kategorie", +"Access Class" => "Klasa dostępu", +"All Day Event" => "Wydarzenie całodniowe", +"From" => "Od", +"To" => "Do", +"Advanced options" => "Opcje zaawansowane", +"Location" => "Lokalizacja", +"Location of the Event" => "Lokalizacja wydarzenia", +"Description" => "Opis", +"Description of the Event" => "Opis wydarzenia", +"Repeat" => "Powtarzaj", +"Advanced" => "Zaawansowane", +"Select weekdays" => "Wybierz dni powszednie", +"Select days" => "Wybierz dni", +"and the events day of year." => "oraz wydarzenia w trakcie roku.", +"and the events day of month." => "oraz wydarzenia w trakcie miesiąca.", +"Select months" => "Wybierz miesiące", +"Select weeks" => "Wybierz tygodnie", +"and the events week of year." => "oraz wydarzenia w trakcie roku.", +"Interval" => "Odstęp", +"End" => "Koniec", +"occurrences" => "wystąpienia", +"create a new calendar" => "stwórz nowy kalendarz", +"Import a calendar file" => "Zaimportuj plik kalendarza", +"Please choose a calendar" => "Wybierz kalendarz", +"Name of new calendar" => "Nazwa nowego kalendarza", +"Take an available name!" => "Wybierz dostępną nazwę!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Kalendarz o tej nazwie już istnieje. Jeśli będziesz kontynuować, kalendarze zostaną scalone.", +"Remove all events from the selected calendar" => "Usuń wszystkie wydarzenia z wybranego kalendarza.", +"Import" => "Importuj", +"Close Dialog" => "Zamknij okno", +"Create a new event" => "Utwórz nowe wydarzenie", +"Share with:" => "Współdziel z:", +"Shared with" => "Współdzielone z:", +"Unshare" => "Zatrzymaj współdzielenie", +"Nobody" => "Nikt", +"Shared via calendar" => "Udostępnione za pośrednictwem kalendarza", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "UWAGA: Czynności wykonane na wydarzeniach udostępnianych poprzez kalendarz mają wpływ na współdzielenie całego kalendarza.", +"View an event" => "Wyświetl wydarzenie", +"No categories selected" => "Nie zaznaczono kategorii", +"of" => "z", +"at" => "w", +"General" => "Ogólne", +"Timezone" => "Strefa czasowa", +"Update timezone automatically" => "Automatyczne aktualizuj strefę czasową", +"Time format" => "Format czasu", +"24h" => "24-godzinny", +"12h" => "12-godzinny", +"Start week on" => "Zaczynaj tydzień od", +"Cache" => "Pamięć podręczna", +"Clear cache for repeating events" => "Wyczyść pamięć podręczną dla powtarzających się zdarzeń", +"URLs" => "Adresy URL", +"Calendar CalDAV syncing addresses" => "Adres synchronizacji kalendarza CalDAV", +"more info" => "więcej informacji", +"Primary address (Kontact et al)" => "Podstawowy adres", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Odnośniki iCalendar tylko do odczytu" +); diff --git a/l10n/pl_PL.php b/l10n/pl_PL.php new file mode 100644 index 000000000..421efcae3 --- /dev/null +++ b/l10n/pl_PL.php @@ -0,0 +1,5 @@ + "Tytuł", +"Settings" => "Ustawienia", +"Save" => "Zapisz" +); diff --git a/l10n/pt_BR.php b/l10n/pt_BR.php new file mode 100644 index 000000000..a2c44e128 --- /dev/null +++ b/l10n/pt_BR.php @@ -0,0 +1,215 @@ + "Nem todos os calendários estão completamente em cache.", +"Everything seems to be completely cached" => "Parece que tudo foi cacheado", +"No calendars found." => "Nenhum calendário encontrado.", +"No events found." => "Nenhum evento encontrado.", +"Wrong calendar" => "Calendário incorreto", +"You do not have the permissions to edit this event." => "Você não tem permissões para editar esse evento.", +"The file contained either no events or all events are already saved in your calendar." => "O arquivo não continha nenhum evento ou todos os eventos já estão guardados no seu calendário.", +"events has been saved in the new calendar" => "os eventos foram salvos no novo calendário", +"Import failed" => "Falha na importação", +"events has been saved in your calendar" => "os eventos foram salvos em seu calendário", +"New Timezone:" => "Novo fuso horário", +"Timezone changed" => "Fuso horário alterado", +"Invalid request" => "Pedido inválido", +"Calendar" => "Calendário", +"Deletion failed" => "Remoção falhou", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ -[ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "usuário", +"group" => "grupo", +"Editable" => "Editável", +"Shareable" => "Compartilhável", +"Deletable" => "Removível", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Domingo", +"Monday" => "Segunda-feira", +"Tuesday" => "Terça-feira", +"Wednesday" => "Quarta-feira", +"Thursday" => "Quinta-feira", +"Friday" => "Sexta-feira", +"Saturday" => "Sábado", +"Sun." => "Dom.", +"Mon." => "Seg.", +"Tue." => "Ter.", +"Wed." => "Qua.", +"Thu." => "Qui.", +"Fri." => "Sex.", +"Sat." => "Sáb.", +"January" => "Janeiro", +"February" => "Fevereiro", +"March" => "Março", +"April" => "Abril", +"May" => "Maio", +"June" => "Junho", +"July" => "Julho", +"August" => "Agosto", +"September" => "Setembro", +"October" => "Outubro", +"November" => "Novembro", +"December" => "Dezembro", +"Jan." => "Jan.", +"Feb." => "Fev.", +"Mar." => "Mar.", +"Apr." => "Abr.", +"May." => "Mai.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Ago.", +"Sep." => "Set.", +"Oct." => "Out.", +"Nov." => "Nov.", +"Dec." => "Dez.", +"All day" => "Todo o dia", +"New Calendar" => "Novo Calendário", +"Missing or invalid fields" => "Campos faltando ou inválidos", +"Title" => "Título", +"From Date" => "Desde a Data", +"From Time" => "Desde a Hora", +"To Date" => "Até a Data", +"To Time" => "Até a Hora", +"The event ends before it starts" => "O evento termina antes de começar", +"There was a database fail" => "Houve uma falha de banco de dados", +"Birthday" => "Aniversário", +"Business" => "Negócio", +"Call" => "Chamada", +"Clients" => "Clientes", +"Deliverer" => "Entrega", +"Holidays" => "Feriados", +"Ideas" => "Idéias", +"Journey" => "Jornada", +"Jubilee" => "Jubileu", +"Meeting" => "Reunião", +"Other" => "Outros", +"Personal" => "Pessoal", +"Projects" => "Projetos", +"Questions" => "Perguntas", +"Work" => "Trabalho", +"by" => "por", +"unnamed" => "sem nome", +"You do not have the permissions to update this calendar." => "Você não tem permissões para atualizar esse calendário.", +"You do not have the permissions to delete this calendar." => "Você não tem permissões para remover esse calendário.", +"You do not have the permissions to add to this calendar." => "Você não tem permissões para adicionar esse calendário.", +"You do not have the permissions to add events to this calendar." => "Você não tem permissões para adicionar eventos nesse calendário.", +"You do not have the permissions to delete this event." => "Você não tem permissões para remover esse evento.", +"Busy" => "Ocupado", +"Public" => "Público", +"Private" => "Privado", +"Confidential" => "Confidencial", +"Does not repeat" => "Não repetir", +"Daily" => "Diariamente", +"Weekly" => "Semanal", +"Every Weekday" => "Cada dia da semana", +"Bi-Weekly" => "De duas em duas semanas", +"Monthly" => "Mensal", +"Yearly" => "Anual", +"never" => "nunca", +"by occurrences" => "por ocorrências", +"by date" => "por data", +"by monthday" => "por dia do mês", +"by weekday" => "por dia da semana", +"events week of month" => "semana do evento no mês", +"first" => "primeiro", +"second" => "segundo", +"third" => "terceiro", +"fourth" => "quarto", +"fifth" => "quinto", +"last" => "último", +"by events date" => "eventos por data", +"by yearday(s)" => "por dia(s) do ano", +"by weeknumber(s)" => "por número(s) da semana", +"by day and month" => "por dia e mês", +"Date" => "Data", +"Cal." => "Cal.", +"Week" => "Semana", +"Month" => "Mês", +"List" => "Lista", +"Today" => "Hoje", +"Settings" => "Ajustes", +"Your calendars" => "Meus Calendários", +"CalDav Link" => "Link para CalDav", +"Share Calendar" => "Compartilhar Calendário", +"Download" => "Baixar", +"Edit" => "Editar", +"Delete" => "Excluir", +"New calendar" => "Novo calendário", +"Edit calendar" => "Editar calendário", +"Displayname" => "Mostrar Nome", +"Active" => "Ativo", +"Calendar color" => "Cor do Calendário", +"Save" => "Salvar", +"Submit" => "Submeter", +"Cancel" => "Cancelar", +"Edit an event" => "Editar um evento", +"Export" => "Exportar", +"Eventinfo" => "Info de Evento", +"Repeating" => "Repetindo", +"Alarm" => "Alarme", +"Attendees" => "Participantes", +"Share" => "Compartilhar", +"Title of the Event" => "Título do evento", +"Category" => "Categoria", +"Separate categories with commas" => "Separe as categorias por vírgulas", +"Edit categories" => "Editar categorias", +"Access Class" => "Classe de Acesso", +"All Day Event" => "Evento de dia inteiro", +"From" => "De", +"To" => "Para", +"Advanced options" => "Opções avançadas", +"Location" => "Local", +"Location of the Event" => "Local do evento", +"Description" => "Descrição", +"Description of the Event" => "Descrição do Evento", +"Repeat" => "Repetir", +"Advanced" => "Avançado", +"Select weekdays" => "Selecionar dias da semana", +"Select days" => "Selecionar dias", +"and the events day of year." => "e o dia do evento no ano.", +"and the events day of month." => "e o dia do evento no mês.", +"Select months" => "Selecionar meses", +"Select weeks" => "Selecionar semanas", +"and the events week of year." => "e a semana do evento no ano.", +"Interval" => "Intervalo", +"End" => "Final", +"occurrences" => "ocorrências", +"create a new calendar" => "criar um novo calendário", +"Import a calendar file" => "Importar um arquivo de calendário", +"Please choose a calendar" => "Escolha um calendário, por favor", +"Name of new calendar" => "Nome do novo calendário", +"Take an available name!" => "Use um nome disponível!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Um calendário com este nome já existe. Se de qualquer forma você continuar, estes calendários serão fundidos.", +"Remove all events from the selected calendar" => "Remover todos os eventos do calendário selecionado", +"Import" => "Importar", +"Close Dialog" => "Fechar caixa de diálogo", +"Create a new event" => "Criar um novo evento", +"Share with:" => "Compartilhar com:", +"Shared with" => "Compartilhado com", +"Unshare" => "Descompartilhar", +"Nobody" => "Ninguém", +"Shared via calendar" => "Compartilhado via calendário", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTA: Ações em eventos compartilhados via calendário afetarão o compartilhamento do calendário inteiro.", +"View an event" => "Visualizar evento", +"No categories selected" => "Nenhuma categoria selecionada", +"of" => "de", +"at" => "para", +"General" => "Geral", +"Timezone" => "Fuso horário", +"Update timezone automatically" => "Atualizar fuso-horário automaticamente", +"Time format" => "Formato de tempo", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Começar semana em", +"Cache" => "Cache", +"Clear cache for repeating events" => "Apagar cache para eventos contínuos", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Sincronização de endereços do calendário CalDAV", +"more info" => "mais informações", +"Primary address (Kontact et al)" => "Endereço primário (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "iCalendar link(s) somente leitura" +); diff --git a/l10n/pt_PT.php b/l10n/pt_PT.php new file mode 100644 index 000000000..97ca7335e --- /dev/null +++ b/l10n/pt_PT.php @@ -0,0 +1,215 @@ + "Nem todos os calendários estão completamente pré-carregados", +"Everything seems to be completely cached" => "Parece que tudo está completamente pré-carregado", +"No calendars found." => "Nenhum calendário encontrado.", +"No events found." => "Nenhum evento encontrado.", +"Wrong calendar" => "Calendário errado", +"You do not have the permissions to edit this event." => "Não tem permissões para alterar este evento.", +"The file contained either no events or all events are already saved in your calendar." => "O ficheiro não continha nenhuns eventos ou então todos os eventos já estavam carregados no seu calendário", +"events has been saved in the new calendar" => "Os eventos foram guardados no novo calendário", +"Import failed" => "Falha na importação", +"events has been saved in your calendar" => "Os eventos foram guardados no seu calendário", +"New Timezone:" => "Nova zona horária", +"Timezone changed" => "Zona horária alterada", +"Invalid request" => "Pedido inválido", +"Calendar" => "Calendário", +"Deletion failed" => "Erro ao apagar", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "utilizador", +"group" => "grupo", +"Editable" => "Editável", +"Shareable" => "Partilhável", +"Deletable" => "Apagável", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Domingo", +"Monday" => "Segunda", +"Tuesday" => "Terça", +"Wednesday" => "Quarta", +"Thursday" => "Quinta", +"Friday" => "Sexta", +"Saturday" => "Sábado", +"Sun." => "Dom.", +"Mon." => "Seg.", +"Tue." => "ter.", +"Wed." => "Qua.", +"Thu." => "Qui.", +"Fri." => "Sex.", +"Sat." => "Sáb.", +"January" => "Janeiro", +"February" => "Fevereiro", +"March" => "Março", +"April" => "Abril", +"May" => "Maio", +"June" => "Junho", +"July" => "Julho", +"August" => "Agosto", +"September" => "Setembro", +"October" => "Outubro", +"November" => "Novembro", +"December" => "Dezembro", +"Jan." => "Jan.", +"Feb." => "Fev,", +"Mar." => "Mar.", +"Apr." => "Abr.", +"May." => "Mai.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Ago.", +"Sep." => "Set.", +"Oct." => "Out.", +"Nov." => "Nov.", +"Dec." => "Dez.", +"All day" => "Todo o dia", +"New Calendar" => "Novo Calendário", +"Missing or invalid fields" => "Campos em falta, ou inválidos!", +"Title" => "Título", +"From Date" => "Da Data", +"From Time" => "Da Hora", +"To Date" => "Para Data", +"To Time" => "Para Hora", +"The event ends before it starts" => "O evento acaba antes de começar", +"There was a database fail" => "Houve uma falha de base de dados", +"Birthday" => "Aniversário", +"Business" => "Negócio", +"Call" => "Telefonar", +"Clients" => "Clientes", +"Deliverer" => "Entregar", +"Holidays" => "Férias", +"Ideas" => "Ideias", +"Journey" => "Jornada", +"Jubilee" => "Jublieu", +"Meeting" => "Reunião", +"Other" => "Outro", +"Personal" => "Pessoal", +"Projects" => "Projectos", +"Questions" => "Perguntas", +"Work" => "Trabalho", +"by" => "por", +"unnamed" => "não definido", +"You do not have the permissions to update this calendar." => "Não tem permissões para alterar este calendário.", +"You do not have the permissions to delete this calendar." => "Não tem permissões para apagar este calendário.", +"You do not have the permissions to add to this calendar." => "Não tem permissões para acrescentar a este calendário.", +"You do not have the permissions to add events to this calendar." => "Não tem permissões para acrescentar eventos a este calendário.", +"You do not have the permissions to delete this event." => "Não tem permissões para apagar este evento.", +"Busy" => "Ocupado", +"Public" => "Público", +"Private" => "Privado", +"Confidential" => "Confidencial", +"Does not repeat" => "Não repete", +"Daily" => "Diário", +"Weekly" => "Semanal", +"Every Weekday" => "Todos os dias da semana", +"Bi-Weekly" => "Bi-semanal", +"Monthly" => "Mensal", +"Yearly" => "Anual", +"never" => "nunca", +"by occurrences" => "por ocorrências", +"by date" => "por data", +"by monthday" => "por dia do mês", +"by weekday" => "por dia da semana", +"events week of month" => "Eventos da semana do mês", +"first" => "primeiro", +"second" => "segundo", +"third" => "terçeiro", +"fourth" => "quarto", +"fifth" => "quinto", +"last" => "último", +"by events date" => "por data de evento", +"by yearday(s)" => "por dia(s) do ano", +"by weeknumber(s)" => "por número(s) da semana", +"by day and month" => "por dia e mês", +"Date" => "Data", +"Cal." => "Cal.", +"Week" => "Semana", +"Month" => "Mês", +"List" => "Lista", +"Today" => "Hoje", +"Settings" => "Configurações", +"Your calendars" => "Os seus calendários", +"CalDav Link" => "Endereço CalDav", +"Share Calendar" => "Partilhar calendário", +"Download" => "Transferir", +"Edit" => "Editar", +"Delete" => "Apagar", +"New calendar" => "Novo calendário", +"Edit calendar" => "Editar calendário", +"Displayname" => "Nome de exibição", +"Active" => "Activo", +"Calendar color" => "Cor do calendário", +"Save" => "Guardar", +"Submit" => "Submeter", +"Cancel" => "Cancelar", +"Edit an event" => "Editar um evento", +"Export" => "Exportar", +"Eventinfo" => "Informação do evento", +"Repeating" => "Repetição", +"Alarm" => "Alarme", +"Attendees" => "Participantes", +"Share" => "Partilhar", +"Title of the Event" => "Título do evento", +"Category" => "Categoria", +"Separate categories with commas" => "Separe categorias por virgulas", +"Edit categories" => "Editar categorias", +"Access Class" => "Classe de Acesso", +"All Day Event" => "Evento de dia inteiro", +"From" => "De", +"To" => "Para", +"Advanced options" => "Opções avançadas", +"Location" => "Localização", +"Location of the Event" => "Localização do evento", +"Description" => "Descrição", +"Description of the Event" => "Descrição do evento", +"Repeat" => "Repetir", +"Advanced" => "Avançado", +"Select weekdays" => "Seleciona os dias da semana", +"Select days" => "Seleciona os dias", +"and the events day of year." => "e o dia de eventos do ano.", +"and the events day of month." => "e o dia de eventos do mês.", +"Select months" => "Selecciona os meses", +"Select weeks" => "Selecciona as semanas", +"and the events week of year." => "e a semana de eventos do ano.", +"Interval" => "Intervalo", +"End" => "Fim", +"occurrences" => "ocorrências", +"create a new calendar" => "criar novo calendário", +"Import a calendar file" => "Importar um ficheiro de calendário", +"Please choose a calendar" => "Escolha um calendário por favor", +"Name of new calendar" => "Nome do novo calendário", +"Take an available name!" => "Escolha um nome disponível!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Já existe um Calendário com esse nome. Se mesmo assim continuar, estes calendários serão fundidos.", +"Remove all events from the selected calendar" => "Remover todos os eventos do calendário seleccionado", +"Import" => "Importar", +"Close Dialog" => "Fechar diálogo", +"Create a new event" => "Criar novo evento", +"Share with:" => "Partilhar com:", +"Shared with" => "Partilhado com:", +"Unshare" => "Deixar de partilhar", +"Nobody" => "Ninguém", +"Shared via calendar" => "Partilhado via calendário", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTA: Acções ou eventos partilhados via calendário irão afectar toda a partilha de calendários", +"View an event" => "Ver um evento", +"No categories selected" => "Nenhuma categoria seleccionada", +"of" => "de", +"at" => "em", +"General" => "Geral", +"Timezone" => "Zona horária", +"Update timezone automatically" => "Actualizar automaticamente o fuso horário", +"Time format" => "Formato da hora", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Começar semana em", +"Cache" => "Memória de pré-carregamento", +"Clear cache for repeating events" => "Limpar a memória de pré carregamento para eventos recorrentes", +"URLs" => "Endereço(s) web", +"Calendar CalDAV syncing addresses" => "Endereços de sincronização de calendários CalDAV", +"more info" => "mais informação", +"Primary address (Kontact et al)" => "Endereço principal (Kontact et al.)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Ligaç(ão/ões) só de leitura do iCalendar" +); diff --git a/l10n/ro.php b/l10n/ro.php new file mode 100644 index 000000000..e8d0bd82f --- /dev/null +++ b/l10n/ro.php @@ -0,0 +1,215 @@ + "Nu toate calendarele sunt salvate in cache", +"Everything seems to be completely cached" => "Totul pare a fi salvat în cache", +"No calendars found." => "Nici un calendar găsit.", +"No events found." => "Nici un eveniment găsit.", +"Wrong calendar" => "Calendar greșit", +"You do not have the permissions to edit this event." => "Nu ai permisiunile necesare pentru a edita acest eveniment.", +"The file contained either no events or all events are already saved in your calendar." => "Fișierul nu conținea nici nu eveniment sau toate evenimentele sunt deja salvate în calendar.", +"events has been saved in the new calendar" => "evenimentele au fost salvate în noul calendar", +"Import failed" => "importul a eșuat", +"events has been saved in your calendar" => "evenimentul a fost salvat în calendar", +"New Timezone:" => "Fus orar nou:", +"Timezone changed" => "Fus orar schimbat", +"Invalid request" => "Cerere eronată", +"Calendar" => "Calendar", +"Deletion failed" => "Ștergerea a eșuat", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "utilizator", +"group" => "grup", +"Editable" => "Editabil", +"Shareable" => "Partajabil", +"Deletable" => "Se poate șterge", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "LLL z[aaaa]{'—'[LLL] z aaaa}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Duminică", +"Monday" => "Luni", +"Tuesday" => "Marți", +"Wednesday" => "Miercuri", +"Thursday" => "Joi", +"Friday" => "Vineri", +"Saturday" => "Sâmbătă", +"Sun." => "Dum.", +"Mon." => "Lun.", +"Tue." => "Mar.", +"Wed." => "Mie.", +"Thu." => "Joi", +"Fri." => "Vin.", +"Sat." => "Sâm.", +"January" => "Ianuarie", +"February" => "Februarie", +"March" => "Martie", +"April" => "Aprilie", +"May" => "Mai", +"June" => "Iunie", +"July" => "Iulie", +"August" => "August", +"September" => "Septembrie", +"October" => "Octombrie", +"November" => "Noiembrie", +"December" => "Decembrie", +"Jan." => "Ian.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Apr.", +"May." => "Mai", +"Jun." => "Iun.", +"Jul." => "Iul.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Oct.", +"Nov." => "Noi.", +"Dec." => "Dec.", +"All day" => "Toată ziua", +"New Calendar" => "Calendar nou", +"Missing or invalid fields" => "Campuri necompletate sau invalide", +"Title" => "Titlu", +"From Date" => "Începând cu", +"From Time" => "De la", +"To Date" => "Până pe", +"To Time" => "La", +"The event ends before it starts" => "Evenimentul se termină înainte să înceapă", +"There was a database fail" => "A avut loc o eroare a bazei de date", +"Birthday" => "Zi de naștere", +"Business" => "Afaceri", +"Call" => "Sună", +"Clients" => "Clienți", +"Deliverer" => "Curier", +"Holidays" => "Sărbători", +"Ideas" => "Idei", +"Journey" => "Călătorie", +"Jubilee" => "Aniversare", +"Meeting" => "Întâlnire", +"Other" => "Altele", +"Personal" => "Personal", +"Projects" => "Proiecte", +"Questions" => "Întrebări", +"Work" => "Servici", +"by" => "după", +"unnamed" => "fără nume", +"You do not have the permissions to update this calendar." => "Nu ai permisiunile necesare pentru a actualiza acest calendar.", +"You do not have the permissions to delete this calendar." => "Nu ai permisiunile necesare pentru a așterge acest calendar.", +"You do not have the permissions to add to this calendar." => "Nu ai permisiuni pentru a face adăugiri în acest calendar.", +"You do not have the permissions to add events to this calendar." => "Nu ai permisiunile necesare pentru a adăuga evenimente la acest calendar.", +"You do not have the permissions to delete this event." => "Nu ai permisiunile necesare pentru a șterge acest eveniment.", +"Busy" => "Ocupat", +"Public" => "Public", +"Private" => "Privat", +"Confidential" => "Confidential", +"Does not repeat" => "Nerepetabil", +"Daily" => "Zilnic", +"Weekly" => "Săptămânal", +"Every Weekday" => "În fiecare zii a săptămânii", +"Bi-Weekly" => "La fiecare două săptămâni", +"Monthly" => "Lunar", +"Yearly" => "Anual", +"never" => "niciodată", +"by occurrences" => "după repetiție", +"by date" => "după dată", +"by monthday" => "după ziua lunii", +"by weekday" => "după ziua săptămânii", +"events week of month" => "evenimentele săptămânii din luna", +"first" => "primul", +"second" => "al doilea", +"third" => "al treilea", +"fourth" => "al patrulea", +"fifth" => "al cincilea", +"last" => "ultimul", +"by events date" => "după data evenimentului", +"by yearday(s)" => "după ziua(zilele) anului", +"by weeknumber(s)" => "după numărul săptămânii", +"by day and month" => "după zi și lună", +"Date" => "Data", +"Cal." => "Cal.", +"Week" => "Săptămâna", +"Month" => "Luna", +"List" => "Listă", +"Today" => "Astăzi", +"Settings" => "Setări", +"Your calendars" => "Calendarele tale", +"CalDav Link" => "Legătură CalDav", +"Share Calendar" => "Partajați calendarul", +"Download" => "Descarcă", +"Edit" => "Modifică", +"Delete" => "Șterge", +"New calendar" => "Calendar nou", +"Edit calendar" => "Modifică calendarul", +"Displayname" => "Nume afișat", +"Active" => "Activ", +"Calendar color" => "Culoarea calendarului", +"Save" => "Salveză", +"Submit" => "Trimite", +"Cancel" => "Anulează", +"Edit an event" => "Modifică un eveniment", +"Export" => "Exportă", +"Eventinfo" => "Informații despre eveniment", +"Repeating" => "Ciclic", +"Alarm" => "Alarmă", +"Attendees" => "Participanți", +"Share" => "Partajează", +"Title of the Event" => "Numele evenimentului", +"Category" => "Categorie", +"Separate categories with commas" => "Separă categoriile prin virgule", +"Edit categories" => "Editează categorii", +"Access Class" => "Clasa de Acces", +"All Day Event" => "Toată ziua", +"From" => "De la", +"To" => "Către", +"Advanced options" => "Opțiuni avansate", +"Location" => "Locație", +"Location of the Event" => "Locația evenimentului", +"Description" => "Descriere", +"Description of the Event" => "Descrierea evenimentului", +"Repeat" => "Repetă", +"Advanced" => "Avansat", +"Select weekdays" => "Selectează zilele săptămânii", +"Select days" => "Selectează zilele", +"and the events day of year." => "și evenimentele de zi cu zi ale anului.", +"and the events day of month." => "și evenimentele de zi cu zi ale lunii.", +"Select months" => "Selectează lunile", +"Select weeks" => "Selectează săptămânile", +"and the events week of year." => "și evenimentele săptămânale ale anului.", +"Interval" => "Interval", +"End" => "Sfârșit", +"occurrences" => "repetiții", +"create a new calendar" => "crează un calendar nou", +"Import a calendar file" => "Importă un calendar", +"Please choose a calendar" => "Vă rugăm să alegeți un calendar", +"Name of new calendar" => "Numele noului calendar", +"Take an available name!" => "Alegeți un nume disponibil!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Un calendar cu acest nume există deja. Dacă oricum veți continua, aceste calendare vor fi fuzionate.", +"Remove all events from the selected calendar" => "Sterge toate evenimentele selectate din calendar", +"Import" => "Importă", +"Close Dialog" => "Închide", +"Create a new event" => "Crează un eveniment nou", +"Share with:" => "Partajează cu:", +"Shared with" => "Partajat cu:", +"Unshare" => "Anulează partajarea", +"Nobody" => "Nimeni", +"Shared via calendar" => "Partajat prin calendar", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTĂ: Acțiunile asupra evenimentelor partajate prin calendar vor fi aplicate la partajarea intregului calendar.", +"View an event" => "Vizualizează un eveniment", +"No categories selected" => "Nici o categorie selectată", +"of" => "din", +"at" => "la", +"General" => "Generale", +"Timezone" => "Fus orar", +"Update timezone automatically" => "Actualizați fusul orar automat", +"Time format" => "Formatul timpului", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Începe săptămâna cu", +"Cache" => "Cache", +"Clear cache for repeating events" => "Curăță cache pentru evenimente repetate", +"URLs" => "linkuri", +"Calendar CalDAV syncing addresses" => "Adrese de sincronizare calendar CalDAV", +"more info" => "mai multe informații", +"Primary address (Kontact et al)" => "Adresa primară (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Citeste numai linkuri iCalendar" +); diff --git a/l10n/ru.php b/l10n/ru.php new file mode 100644 index 000000000..8ee250c63 --- /dev/null +++ b/l10n/ru.php @@ -0,0 +1,215 @@ + "Не все календари полностью кешированы", +"Everything seems to be completely cached" => "Все, вроде бы, закешировано", +"No calendars found." => "Календари не найдены.", +"No events found." => "События не найдены.", +"Wrong calendar" => "Неверный календарь", +"You do not have the permissions to edit this event." => "У вас нет права редактировать это событие.", +"The file contained either no events or all events are already saved in your calendar." => "Файл либо не собержит событий, либо все события уже есть в календаре", +"events has been saved in the new calendar" => "события были сохранены в новый календарь", +"Import failed" => "Ошибка импорта", +"events has been saved in your calendar" => "события были сохранены в вашем календаре", +"New Timezone:" => "Новый часовой пояс:", +"Timezone changed" => "Часовой пояс изменён", +"Invalid request" => "Неверный запрос", +"Calendar" => "Календарь", +"Deletion failed" => "Удаление не удалось", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "пользователь", +"group" => "группа", +"Editable" => "Редактирование", +"Shareable" => "Публикация", +"Deletable" => "Удаление", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Воскресенье", +"Monday" => "Понедельник", +"Tuesday" => "Вторник", +"Wednesday" => "Среда", +"Thursday" => "Четверг", +"Friday" => "Пятница", +"Saturday" => "Суббота", +"Sun." => "Вс.", +"Mon." => "Пн.", +"Tue." => "Вт.", +"Wed." => "Ср.", +"Thu." => "Чт.", +"Fri." => "Пт.", +"Sat." => "Сб.", +"January" => "Январь", +"February" => "Февраль", +"March" => "Март", +"April" => "Апрель", +"May" => "Май", +"June" => "Июнь", +"July" => "Июль", +"August" => "Август", +"September" => "Сентябрь", +"October" => "Октябрь", +"November" => "Ноябрь", +"December" => "Декабрь", +"Jan." => "Янв.", +"Feb." => "Фев.", +"Mar." => "Мар.", +"Apr." => "Апр.", +"May." => "Май.", +"Jun." => "Июн.", +"Jul." => "Июл.", +"Aug." => "Авг.", +"Sep." => "Сен.", +"Oct." => "Окт.", +"Nov." => "Ноя.", +"Dec." => "Дек.", +"All day" => "Весь день", +"New Calendar" => "Новый Календарь", +"Missing or invalid fields" => "Некоторые поля отсутствуют или некорректны", +"Title" => "Название", +"From Date" => "Дата начала", +"From Time" => "Время начала", +"To Date" => "Дата окончания", +"To Time" => "Время окончания", +"The event ends before it starts" => "Окончание события раньше, чем его начало", +"There was a database fail" => "Ошибка базы данных", +"Birthday" => "День рождения", +"Business" => "Бизнес", +"Call" => "Звонить", +"Clients" => "Клиенты", +"Deliverer" => "Посыльный", +"Holidays" => "Праздники", +"Ideas" => "Идеи", +"Journey" => "Поездка", +"Jubilee" => "Юбилей", +"Meeting" => "Встреча", +"Other" => "Другое", +"Personal" => "Личное", +"Projects" => "Проекты", +"Questions" => "Вопросы", +"Work" => "Работа", +"by" => "до свидания", +"unnamed" => "без имени", +"You do not have the permissions to update this calendar." => "У вас нет права изменять этот календарь.", +"You do not have the permissions to delete this calendar." => "У вас нет права удалять этот календарь", +"You do not have the permissions to add to this calendar." => "У вас нет права добавлять в этот календарь.", +"You do not have the permissions to add events to this calendar." => "У вас нет права создавать события в этом календаре.", +"You do not have the permissions to delete this event." => "У вас нет права удалить это событие.", +"Busy" => "Занято", +"Public" => "Общедоступно", +"Private" => "Частное", +"Confidential" => "Конфиденциально", +"Does not repeat" => "Не повторяется", +"Daily" => "Ежедневно", +"Weekly" => "Еженедельно", +"Every Weekday" => "По будням", +"Bi-Weekly" => "Каждые две недели", +"Monthly" => "Каждый месяц", +"Yearly" => "Каждый год", +"never" => "никогда", +"by occurrences" => "по числу повторений", +"by date" => "по дате", +"by monthday" => "по дню месяца", +"by weekday" => "по дню недели", +"events week of month" => "неделя месяца", +"first" => "первая", +"second" => "вторая", +"third" => "третья", +"fourth" => "червётрая", +"fifth" => "пятая", +"last" => "последняя", +"by events date" => "по дате событий", +"by yearday(s)" => "по дням недели", +"by weeknumber(s)" => "по номерам недели", +"by day and month" => "по дню и месяцу", +"Date" => "Дата", +"Cal." => "Кал.", +"Week" => "Неделя", +"Month" => "Месяц", +"List" => "Список", +"Today" => "Сегодня", +"Settings" => "Параметры", +"Your calendars" => "Ваши календари", +"CalDav Link" => "Ссылка для CalDav", +"Share Calendar" => "Опубликовать", +"Download" => "Скачать", +"Edit" => "Редактировать", +"Delete" => "Удалить", +"New calendar" => "Новый календарь", +"Edit calendar" => "Редактировать календарь", +"Displayname" => "Отображаемое имя", +"Active" => "Активен", +"Calendar color" => "Цвет календаря", +"Save" => "Сохранить", +"Submit" => "Отправить", +"Cancel" => "Отмена", +"Edit an event" => "Редактировать событие", +"Export" => "Экспортировать", +"Eventinfo" => "Информация о событии", +"Repeating" => "Повторение", +"Alarm" => "Сигнал", +"Attendees" => "Участники", +"Share" => "Опубликовать", +"Title of the Event" => "Название событие", +"Category" => "Категория", +"Separate categories with commas" => "Разделяйте категории запятыми", +"Edit categories" => "Редактировать категории", +"Access Class" => "Уровень доступа", +"All Day Event" => "Событие на весь день", +"From" => "От", +"To" => "До", +"Advanced options" => "Дополнительные параметры", +"Location" => "Место", +"Location of the Event" => "Место события", +"Description" => "Описание", +"Description of the Event" => "Описание события", +"Repeat" => "Повтор", +"Advanced" => "Дополнительно", +"Select weekdays" => "Выбрать дни недели", +"Select days" => "Выбрать дни", +"and the events day of year." => "и день года события", +"and the events day of month." => "и день месяца события", +"Select months" => "Выбрать месяцы", +"Select weeks" => "Выбрать недели", +"and the events week of year." => "и номер недели события", +"Interval" => "Интервал", +"End" => "Окончание", +"occurrences" => "повторений", +"create a new calendar" => "Создать новый календарь", +"Import a calendar file" => "Импортировать календарь из файла", +"Please choose a calendar" => "Пожалуйста, выберите календарь", +"Name of new calendar" => "Название нового календаря", +"Take an available name!" => "Возьмите разрешенное имя!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Календарь с таким именем уже существует. Если вы продолжите, одноименный календарь будет удален.", +"Remove all events from the selected calendar" => "Удалить все события из выбранного календаря", +"Import" => "Импортировать", +"Close Dialog" => "Закрыть Сообщение", +"Create a new event" => "Создать новое событие", +"Share with:" => "Опубликовать для:", +"Shared with" => "Опубликован для", +"Unshare" => "Отменить публикацию", +"Nobody" => "Никого", +"Shared via calendar" => "Опубликовано с календарем:", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "Примечание: Изменив настройки публикации для события опубликованного через календарь, вы измените настройки публикации всего календаря.", +"View an event" => "Показать событие", +"No categories selected" => "Категории не выбраны", +"of" => "из", +"at" => "на", +"General" => "Основные", +"Timezone" => "Часовой пояс", +"Update timezone automatically" => "Автоматическое обновление временной зоны", +"Time format" => "Формат времени", +"24h" => "24ч", +"12h" => "12ч", +"Start week on" => "Начало недели", +"Cache" => "Кэш", +"Clear cache for repeating events" => "Очистить кэш повторяющихся событий", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Адрес синхронизации CalDAV", +"more info" => "подробнее", +"Primary address (Kontact et al)" => "Основной адрес (Kontact и др.)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Читать только ссылки iCalendar" +); diff --git a/l10n/ru_RU.php b/l10n/ru_RU.php new file mode 100644 index 000000000..e3b6a2585 --- /dev/null +++ b/l10n/ru_RU.php @@ -0,0 +1,215 @@ + "Не все календари полностью кэшированы", +"Everything seems to be completely cached" => "Все кажется полностью записанным в кэш", +"No calendars found." => "Не найдено календарей", +"No events found." => "Не найдено событий", +"Wrong calendar" => "Неверный календарь", +"You do not have the permissions to edit this event." => "У Вас нет прав доступа для редактирования этого события.", +"The file contained either no events or all events are already saved in your calendar." => "Файл либо не содержит событий, либо все события уже сохранены в Вашем календаре.", +"events has been saved in the new calendar" => "Событие было сохранено в новом календаре ", +"Import failed" => "Импортирование не удалось", +"events has been saved in your calendar" => "Событие было сохранено в Вашем календаре ", +"New Timezone:" => "Новый часовой пояс:", +"Timezone changed" => "Часовой пояс изменен", +"Invalid request" => "Неверный запрос", +"Calendar" => "Календарь", +"Deletion failed" => "Удаление не удалось", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "пользователь", +"group" => "группа", +"Editable" => "Редактируемо", +"Shareable" => "Совместный ", +"Deletable" => "Удаляемый", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Воскресенье", +"Monday" => "Понедельник", +"Tuesday" => "Вторник", +"Wednesday" => "Среда", +"Thursday" => "Четверг", +"Friday" => "Пятница", +"Saturday" => "Суббота", +"Sun." => "Воскр.", +"Mon." => "Пон.", +"Tue." => "Вт.", +"Wed." => "Ср.", +"Thu." => "Четв.", +"Fri." => "Пятн.", +"Sat." => "Субб.", +"January" => "Январь", +"February" => "Февраль", +"March" => "Март", +"April" => "Апрель", +"May" => "Май", +"June" => "Июнь", +"July" => "Июль", +"August" => "Август", +"September" => "Сентябрь", +"October" => "Октябрь", +"November" => "Ноябрь", +"December" => "Декабрь", +"Jan." => "Янв.", +"Feb." => "Февр.", +"Mar." => "Март", +"Apr." => "Апр.", +"May." => "Май", +"Jun." => "Июнь", +"Jul." => "Июль", +"Aug." => "Авг.", +"Sep." => "Сент.", +"Oct." => "Окт.", +"Nov." => "Нояб.", +"Dec." => "Дек.", +"All day" => "Весь день", +"New Calendar" => "Новый календарь", +"Missing or invalid fields" => "Пропущенные или недопустимые поля", +"Title" => "Название", +"From Date" => "С (дата)", +"From Time" => "С (время)", +"To Date" => "По (дата)", +"To Time" => "По (время)", +"The event ends before it starts" => "Событие заканчивается прежде, чем началось", +"There was a database fail" => "Произошел сбой базы данных", +"Birthday" => "День Рождения", +"Business" => "Рабочий", +"Call" => "Звонок", +"Clients" => "Клиенты", +"Deliverer" => "Поставщик", +"Holidays" => "Праздники", +"Ideas" => "Идеи", +"Journey" => "Путешествие", +"Jubilee" => "Юбилей", +"Meeting" => "Встреча", +"Other" => "Другое", +"Personal" => "Персональный", +"Projects" => "Проекты", +"Questions" => "Вопросы", +"Work" => "Работа", +"by" => " ", +"unnamed" => "безымянный", +"You do not have the permissions to update this calendar." => "У Вас нет прав доступа для обновление этого календаря.", +"You do not have the permissions to delete this calendar." => "У Вас нет прав доступа для удаления этого календаря.", +"You do not have the permissions to add to this calendar." => "У Вас нет прав доступа для добавления этого календаря.", +"You do not have the permissions to add events to this calendar." => "У Вас нет прав доступа для добавления событий в этот календарь.", +"You do not have the permissions to delete this event." => "У Вас нет прав доступа для удаления этого события.", +"Busy" => "Занятый", +"Public" => "Публичный", +"Private" => "Частный", +"Confidential" => "Конфиденциальный", +"Does not repeat" => "Не повторяется", +"Daily" => "Ежедневный", +"Weekly" => "Еженедельный", +"Every Weekday" => "Каждый будний день", +"Bi-Weekly" => " Раз в две недели", +"Monthly" => "Ежемесячно", +"Yearly" => "Ежегодно", +"never" => "никогда", +"by occurrences" => "по распространенности", +"by date" => "по дате", +"by monthday" => "по дню месяца", +"by weekday" => "по будням", +"events week of month" => "события недели месяца", +"first" => "первый", +"second" => "второй", +"third" => "третий", +"fourth" => "четвертый", +"fifth" => "пятый", +"last" => "последний", +"by events date" => "по дате событий", +"by yearday(s)" => "по дням года", +"by weeknumber(s)" => "по номеру недели", +"by day and month" => "по дню и месяцу", +"Date" => "Дата", +"Cal." => "Календарь", +"Week" => "Неделя", +"Month" => "Месяц", +"List" => "Список", +"Today" => "Сегодня", +"Settings" => "Настройки", +"Your calendars" => "Ваши календари", +"CalDav Link" => "CalDav ссылка", +"Share Calendar" => "Сделать календарь общим", +"Download" => "Загрузка", +"Edit" => "Редактировать", +"Delete" => "Удалить", +"New calendar" => "Новый календарь", +"Edit calendar" => "Редактировать календарь", +"Displayname" => "Отображаемое имя", +"Active" => "Активный", +"Calendar color" => "Цвет календаря", +"Save" => "Сохранить", +"Submit" => "Принять", +"Cancel" => "Отмена", +"Edit an event" => "Редактировать событие", +"Export" => "Экспорт", +"Eventinfo" => "Информация о событии", +"Repeating" => "Повтор", +"Alarm" => "Сигнал", +"Attendees" => "Участники", +"Share" => "Сделать общим", +"Title of the Event" => "Название события", +"Category" => "Категория", +"Separate categories with commas" => "Отдельные категории, разделенные запятыми", +"Edit categories" => "Редактировать категории", +"Access Class" => "Класс доступа", +"All Day Event" => "Событие на весь день", +"From" => "От", +"To" => "До", +"Advanced options" => "Дополнительные опции", +"Location" => "Местоположение", +"Location of the Event" => "Местоположение события", +"Description" => "Описание", +"Description of the Event" => "Описание события", +"Repeat" => "Повтор", +"Advanced" => "Расширенный", +"Select weekdays" => "Выбрать дни недели", +"Select days" => "Выбрать дни", +"and the events day of year." => "и день года события", +"and the events day of month." => "и день месяца события", +"Select months" => "Выбрать месяцы", +"Select weeks" => "Выбрать недели", +"and the events week of year." => "и неделя года события", +"Interval" => "Интервал", +"End" => "Конец", +"occurrences" => "распространенность", +"create a new calendar" => "создать новый календарь", +"Import a calendar file" => "Импортировать файл календаря", +"Please choose a calendar" => "Пожалуйста, выберите календарь.", +"Name of new calendar" => "Имя нового календаря", +"Take an available name!" => "Возьмите доступное имя!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Календарь с таким именем существует. Если вы будете продолжать так или иначе, эти календари будут объединены.", +"Remove all events from the selected calendar" => "Удалить все события из выбранного календаря", +"Import" => "Импорт", +"Close Dialog" => "Закрыть диалог", +"Create a new event" => "Создать новое событие", +"Share with:" => "Сделать общий доступ с:", +"Shared with" => "Общий доступ с", +"Unshare" => "Отключить общий доступ", +"Nobody" => "Никто", +"Shared via calendar" => "Сделать общим посредством календаря", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "ПРИМЕЧАНИЕ: Действия с событиями, введенными в общий доступ посредством календаря, будут оказывать влияние на весь календарный обмен.", +"View an event" => "Просмотр события", +"No categories selected" => "Не выбрано категорий", +"of" => " ", +"at" => "на", +"General" => "Общие", +"Timezone" => "Часовой пояс", +"Update timezone automatically" => "Автоматически обновлять часовой пояс", +"Time format" => "Формат времени", +"24h" => "24ч", +"12h" => "12ч", +"Start week on" => "Начать неделю с", +"Cache" => "Кэш", +"Clear cache for repeating events" => "Очистить кэш для повторяющихся событий", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Адреса синхронизации CalDAV календаря", +"more info" => "подробная информация", +"Primary address (Kontact et al)" => "Основной адрес (Kontact и др.)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "iCalendar-cсылки только для чтения " +); diff --git a/l10n/si_LK.php b/l10n/si_LK.php new file mode 100644 index 000000000..44f1286d4 --- /dev/null +++ b/l10n/si_LK.php @@ -0,0 +1,213 @@ + "සියලු දිනදසුන් සම්පූර්ණයෙන් හැඹිලිගත නොකල යුතුයි", +"Everything seems to be completely cached" => "දිස්වන සියල්ල සම්පූර්ණයෙන්ම හැඹිලිගත කරන්න", +"No calendars found." => "දිනදසුන් හමුවී නැත.", +"No events found." => "සිදුවීම් හමුවී නැත.", +"Wrong calendar" => "වැරදි දිනදසුනක්", +"You do not have the permissions to edit this event." => "මෙම සිදුවීම සංස්කරණය කිරීම සදහා ඔබට අවසර නොමැත.", +"The file contained either no events or all events are already saved in your calendar." => "ලිපිගොනුවේ තුල කිසිදු සිදුවීමක් නොමැත හෝ සියලු සිදුවීම් දැනටමත් ඔබගේ දිනදසුනේ සුරැකී පවතී.", +"events has been saved in the new calendar" => "නව දිනදසුනක සිදුවීම් සුරකින ලදී", +"Import failed" => "ආයත කිරීම අසාර්ථකයි", +"events has been saved in your calendar" => "ඔබේ දිනදසුනේ සිදුවීම් සුරකින ලදී", +"New Timezone:" => "නව වේලා කලාපය:", +"Timezone changed" => "වේලා කලාපය වෙනස්කරන ලදී.", +"Invalid request" => "අවලංගු අයැදුමක්", +"Calendar" => "දිනදසුන", +"Deletion failed" => "මකාදැමීම අසාර්ථකයි", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "දිදිදි දි මාමාමාමා [ වවවව]{ - [දිදිදි දි] මාමාමාමා වවවව}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "දිදිදි දි මාමාමාමා [ වවවව] පැපැ:මිමි{ - [ දිදිදි දි මාමාමාමා වවවව] පැපැ:මිමි}", +"user" => "පරිශිලකයා", +"group" => "කණ්ඩායම", +"Editable" => "සැකසිය හැක", +"Shareable" => "හවුල් කරගතහැක", +"Deletable" => "මකාදැමිය හැක", +"ddd" => "දිදිදි", +"ddd M/d" => "දිදිදි මා/දි", +"dddd M/d" => "දිදිදිදි මා/දි", +"MMMM yyyy" => "මාමාමාමා වවවව", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "මාමාමා දි[ වවවව]{ '—'[ මාමාමා] දි වවවව}", +"dddd, MMM d, yyyy" => "දිදිදිදි, මාමාමා දි, වවවව", +"Sunday" => "ඉරිදා", +"Monday" => "සඳුදා", +"Tuesday" => "අඟහරුවාදා", +"Wednesday" => "බදාදා", +"Thursday" => "බ්‍රහස්පතින්දා", +"Friday" => "සිකුරාදා", +"Saturday" => "සෙනසුරාදා", +"Sun." => "ඉරිදා", +"Mon." => "සඳුදා", +"Tue." => "අඟ.", +"Wed." => "බදාදා", +"Thu." => "බ්‍රහස්.", +"Fri." => "සිකු.", +"Sat." => "සෙන.", +"January" => "ජනවාරි", +"February" => "පෙබරවාරි", +"March" => "මාර්තු", +"April" => "අප්‍රේල්", +"May" => "මැයි", +"June" => "ජූනි", +"July" => "ජූලි", +"August" => "අගෝස්තු", +"September" => "සැප්තැම්බර්", +"October" => "ඔක්තෝබර", +"November" => "නොවැම්බර්", +"December" => "දෙසැම්බර්", +"Jan." => "ජන.", +"Feb." => "පෙබ.", +"Mar." => "මාර්තු", +"Apr." => "අප්‍රේල්", +"May." => "මැයි", +"Jun." => "ජුනි", +"Jul." => "ජුලි", +"Aug." => "අගෝ.", +"Sep." => "සැප්.", +"Oct." => "ඔක්.", +"Nov." => "නොවැ.", +"Dec." => "දෙසැ.", +"All day" => "සියලු දින", +"New Calendar" => "නව දිනදසුන", +"Title" => "මාතෘකාව", +"From Date" => "දින සිට", +"From Time" => "වේලාවේ සිට", +"To Date" => "දින දක්වා", +"To Time" => "වේලාව දක්වා", +"The event ends before it starts" => "සිදුවීම ආරම්භකිරීමට පෙර අවසන්වුනි", +"There was a database fail" => "දත්ත සමුදායේ දෝෂයක්", +"Birthday" => "උපන්දින", +"Business" => "ව්‍යාපාරික", +"Call" => "ඇමතුම", +"Clients" => "සේවාලාභින්", +"Deliverer" => "බාරදෙන්නා", +"Holidays" => "නිවාඩු දින", +"Ideas" => "අදහස්", +"Journey" => "සංචාරය", +"Jubilee" => "උත්සවය/ජුබිලිය", +"Meeting" => "රැස්වීම", +"Other" => "වෙනත්", +"Personal" => "පෞද්ගලික", +"Projects" => "ව්‍යාපෘති", +"Questions" => "ප්‍රශ්න", +"Work" => "කාර්යය", +"by" => "විසින්", +"unnamed" => "නිර්නාමික", +"You do not have the permissions to update this calendar." => "මෙම දින දසුන යාත්කාලීන කිරීම සදහා ඔබට අවසර නොමැත", +"You do not have the permissions to delete this calendar." => "මෙම දින දසුන මකාදැමිම සදහා ඔබට අවසර නොමැත", +"You do not have the permissions to add to this calendar." => "මෙම දින දසුනට එකතු කිරීම සදහා ඔබට අවසර නොමැත", +"You do not have the permissions to add events to this calendar." => "මෙම දින දසුනට සිදුවීම් එකතු කිරීම සදහා ඔබට අවසර නොමැත", +"You do not have the permissions to delete this event." => "මෙම සිදුවීම මකාදැමීම සදහා ඔබට අවසර නොමැත.", +"Busy" => "කාර්යබහුල", +"Public" => "පොදු", +"Private" => "පෞද්ගලික", +"Confidential" => "රහස්‍ය", +"Does not repeat" => "පුනරාවර්ථන නොවේ", +"Daily" => "දිනපතා", +"Weekly" => "සතිපතා", +"Every Weekday" => "සෑම වැඩකරන දිනකම", +"Bi-Weekly" => "දෙසතියකට වරක්", +"Monthly" => "මාසික", +"Yearly" => "වාර්ෂික", +"never" => "කවදාවත්", +"by occurrences" => "සිදුවීම් අනුව", +"by date" => "නව දිනදසුන", +"by monthday" => "මාසයේ දින අනුව", +"by weekday" => "සතියේ දින අනුව", +"events week of month" => "මාසයකදී සතියක තිබෙන සිදුවීම්", +"first" => "පළමු", +"second" => "දෙවන ", +"third" => "තෙවන", +"fourth" => "සිව්වන", +"fifth" => "පස්වන", +"last" => "අවසාන", +"by events date" => "සිදුවීම් දිනය අනුව", +"by yearday(s)" => "වර්ශයේ දිනයන් අනුව", +"by weeknumber(s)" => "සතියේ අංකය අනුව", +"by day and month" => "දිනය හා මාසය අනුව", +"Date" => "දිනය", +"Cal." => "දිනද.", +"Week" => "සතිය", +"Month" => "මාසය", +"List" => "ලැයිස්තුව", +"Today" => "අද", +"Settings" => "සිටුවම", +"Your calendars" => "ඔබේ දිනදසුන", +"CalDav Link" => "CalDav සබැදිය", +"Share Calendar" => "බෙදාහදාගත් දිනදසුන", +"Download" => "බාන්න", +"Edit" => "සැකසීම", +"Delete" => "මකන්න", +"New calendar" => "නව දින දර්ශනය", +"Edit calendar" => "දිනදසුන සකසන්න", +"Displayname" => "පෙන්වන නම", +"Active" => "සක්‍රීය", +"Calendar color" => "දිනදසුනේ පැහැය", +"Save" => "සුරකින්න", +"Submit" => "යොමු කරන්න", +"Cancel" => "එපා", +"Edit an event" => "සිදුවීම් සංස්කරණය", +"Export" => "නිර්යාත කරන්න", +"Eventinfo" => "සිදුවීම්විස්තරය", +"Repeating" => "පුනරාවර්ථනයන්", +"Alarm" => "සීනුව", +"Attendees" => "කැටුව යන්නන්", +"Share" => "අංශය/පංගුව", +"Title of the Event" => "සිදුවීමේ මාතෘකාව", +"Category" => "ප්‍රභේද", +"Separate categories with commas" => "ප්‍රභේදයන් කොමා වලින් වෙන්කරන්න", +"Edit categories" => "ප්‍රභේදයන් සංස්කරණය", +"All Day Event" => "දිනයේ සියලු සිදුවීම්", +"From" => "සිට", +"To" => "දකවා", +"Advanced options" => "ඉහල විකල්ප", +"Location" => "ස්ථානය", +"Location of the Event" => "සිදුවීම පිළිබද ස්ථානය", +"Description" => "විස්තර", +"Description of the Event" => "සිදුවීම පිළිබද විස්තරය", +"Repeat" => "පුනරාවර්ථන", +"Advanced" => "දියුණු/උසස්", +"Select weekdays" => "තෝරාගත් සතියේ දින", +"Select days" => "තෝරාගත් දිනයන්", +"and the events day of year." => "සහ වසරේ සිදුවීම් සිදුවු දිනය.", +"and the events day of month." => "සහ මාසයේ සිදුවීම් සිදුවු දිනය.", +"Select months" => "තෝරාගත් මාස ", +"Select weeks" => "තෝරාගත් සති", +"and the events week of year." => "සහ වසරේ සිදුවීම් සිදුවු සතිය.", +"Interval" => "විවේකය", +"End" => "අවසානය", +"occurrences" => "සිදුවීම්", +"create a new calendar" => "නව දිනදසුනක් නිර්මාණය කරන්න", +"Import a calendar file" => "දිනදසුන් ගොනුවක් ආයත කරන්න", +"Please choose a calendar" => "කරුණාකර දිනදසුනක් තෝරන්න", +"Name of new calendar" => "නව දිනදසුනේ නම ", +"Take an available name!" => "තිබෙන නමක් ගැනීම!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "මෙම නමින් තිබෙන දිනදසුනක් දැනටම අඩංගුව පවතී. ඔබ මෙය දිගටම කරන්නේනම්, මෙම දිනදසුන ඉදිරියෙදි මුසුවේ.", +"Remove all events from the selected calendar" => "තෝරාගත් දින දර්ශකයේ සඳහන් සියලු සිදුවීම් ඉවත් කරන්න", +"Import" => "ආයාත කරන්න", +"Close Dialog" => "සංවාදය නැවැත්වීම", +"Create a new event" => "නව සිදුවීමක් නිර්මාණය කරන්න", +"Share with:" => "සමග බෙදාහදාගැනීම:", +"Shared with" => "සමග බෙදාහදාගත්", +"Unshare" => "නොබෙදු", +"Nobody" => "කවුරුවත් නෑ", +"Shared via calendar" => "දිනදසුන හරහා බෙදාහදා ගැනීම්", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "සටහන: ඉදිරියේදී සිදුවීම් වල ක්‍රියාකාරකම් දිනදසුන හරහා බෙදාහදා ගැනීම්, සම්පූර්ණ දිනදසුන බෙදාහදා ගැනීමේදි අවධානමකට ලක්විය හැක.", +"View an event" => "සිදුවීම දක්වන්න", +"No categories selected" => "ප්‍රවර්ගයන් කිසිවක් තෝරනු ලැබුවේ නැත", +"of" => "ගේ", +"at" => "දි", +"General" => "සාමාන්‍යයෙන්", +"Timezone" => "වේලා කලාපය", +"Update timezone automatically" => "ස්වයංක්‍රීයව වේලා කලාපය යාවත්කාලීන කිරිම.", +"Time format" => "කාල හැඩතලය", +"24h" => "පැය 24", +"12h" => "පැය 12", +"Start week on" => "ආරම්භකරන සතිය", +"Cache" => "හැඹිලිය", +"Clear cache for repeating events" => "පුනරාවර්ථන සිදුවීම් සදහා හැඹිලිය හිස් කරන්න", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "දිනදසුන CalDAV syncing ලිපිනය", +"more info" => "වැඩිදුරටත්", +"Primary address (Kontact et al)" => "ප්‍රාථමික ලිපිනය(හැම විටම සම්බන්ධ කරගත හැක)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "iදින දසුන(න්) කියවීම සදහා පමණි " +); diff --git a/l10n/sk_SK.php b/l10n/sk_SK.php new file mode 100644 index 000000000..55d822ffc --- /dev/null +++ b/l10n/sk_SK.php @@ -0,0 +1,215 @@ + "V pamäti nie sú úplne uložené všetky kalendáre", +"Everything seems to be completely cached" => "Zdá sa, že je všetko úplne uložené v pamäti", +"No calendars found." => "Nenašiel sa žiadny kalendár.", +"No events found." => "Nenašla sa žiadna udalosť.", +"Wrong calendar" => "Zlý kalendár", +"You do not have the permissions to edit this event." => "Nemáte oprávnenie pre úpravu tejto udalosti.", +"The file contained either no events or all events are already saved in your calendar." => "Súbor neobsahoval žiadne udalosti, alebo už sú všetky udalosti vo Vašom kalendári.", +"events has been saved in the new calendar" => "udalosti boli zapísané do nového kalendára", +"Import failed" => "Import zlyhal", +"events has been saved in your calendar" => "udalosti boli zapísané do kalendára", +"New Timezone:" => "Nová časová zóna:", +"Timezone changed" => "Časové pásmo zmenené", +"Invalid request" => "Neplatná požiadavka", +"Calendar" => "Kalendár", +"Deletion failed" => "Odstránenie zlyhalo", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "používateľ", +"group" => "skupina", +"Editable" => "Upravovateľné", +"Shareable" => "Zdieľateľný", +"Deletable" => "Zmazateľný", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "d. MMM[ yyyy]{ '—' d.[ MMM] yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Nedeľa", +"Monday" => "Pondelok", +"Tuesday" => "Utorok", +"Wednesday" => "Streda", +"Thursday" => "Štvrtok", +"Friday" => "Piatok", +"Saturday" => "Sobota", +"Sun." => "Ned.", +"Mon." => "Pon.", +"Tue." => "Uto.", +"Wed." => "Str.", +"Thu." => "Štv.", +"Fri." => "Pia.", +"Sat." => "Sob.", +"January" => "Január", +"February" => "Február", +"March" => "Marec", +"April" => "Apríl", +"May" => "Máj", +"June" => "Jún", +"July" => "Júl", +"August" => "August", +"September" => "September", +"October" => "Október", +"November" => "November", +"December" => "December", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Apr.", +"May." => "Máj.", +"Jun." => "Jún.", +"Jul." => "Júl.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dec.", +"All day" => "Celý deň", +"New Calendar" => "Nový kalendár", +"Missing or invalid fields" => "Chýbajúce alebo neplatné polia", +"Title" => "Nadpis", +"From Date" => "Od dátumu", +"From Time" => "Od času", +"To Date" => "Do dátumu", +"To Time" => "Do času", +"The event ends before it starts" => "Udalosť končí ešte pred tým než začne", +"There was a database fail" => "Nastala chyba databázy", +"Birthday" => "Narodeniny", +"Business" => "Podnikanie", +"Call" => "Hovor", +"Clients" => "Klienti", +"Deliverer" => "Doručovateľ", +"Holidays" => "Prázdniny", +"Ideas" => "Nápady", +"Journey" => "Cesta", +"Jubilee" => "Jubileá", +"Meeting" => "Stretnutia", +"Other" => "Ostatné", +"Personal" => "Osobné", +"Projects" => "Projekty", +"Questions" => "Otázky", +"Work" => "Práca", +"by" => "od", +"unnamed" => "nepomenovaný", +"You do not have the permissions to update this calendar." => "Nemáte oprávnenie pre vykonanie zmien v tomto kalendári.", +"You do not have the permissions to delete this calendar." => "Nemáte oprávnenie pre odstránenie kalendára.", +"You do not have the permissions to add to this calendar." => "Nemáte oprávnenie pre pridanie do tohto kalendára.", +"You do not have the permissions to add events to this calendar." => "Nemáte oprávnenie pre pridanie udalostí do tohto kalendára.", +"You do not have the permissions to delete this event." => "Nemáte oprávnenie pre odstránenie tejto udalosti.", +"Busy" => "Zaneprázdený", +"Public" => "Verejný", +"Private" => "Súkromný", +"Confidential" => "Tajný", +"Does not repeat" => "Neopakovať", +"Daily" => "Denne", +"Weekly" => "Týždenne", +"Every Weekday" => "Každý deň v týždni", +"Bi-Weekly" => "Každý druhý týždeň", +"Monthly" => "Mesačne", +"Yearly" => "Ročne", +"never" => "nikdy", +"by occurrences" => "podľa výskytu", +"by date" => "podľa dátumu", +"by monthday" => "podľa dňa v mesiaci", +"by weekday" => "podľa dňa v týždni", +"events week of month" => "týždenné udalosti v mesiaci", +"first" => "prvý", +"second" => "druhý", +"third" => "tretí", +"fourth" => "štvrtý", +"fifth" => "piaty", +"last" => "posledný", +"by events date" => "podľa dátumu udalosti", +"by yearday(s)" => "po dňoch", +"by weeknumber(s)" => "podľa čísel týždňov", +"by day and month" => "podľa dňa a mesiaca", +"Date" => "Dátum", +"Cal." => "Kal.", +"Week" => "Týždeň", +"Month" => "Mesiac", +"List" => "Zoznam", +"Today" => "Dnes", +"Settings" => "Nastavenia", +"Your calendars" => "Vaše kalendáre", +"CalDav Link" => "CalDav odkaz", +"Share Calendar" => "Zdielať kalendár", +"Download" => "Stiahnuť", +"Edit" => "Upraviť", +"Delete" => "Odstrániť", +"New calendar" => "Nový kalendár", +"Edit calendar" => "Upraviť kalendár", +"Displayname" => "Zobrazené meno", +"Active" => "Aktívne", +"Calendar color" => "Farba kalendára", +"Save" => "Uložiť", +"Submit" => "Odoslať", +"Cancel" => "Zrušiť", +"Edit an event" => "Upraviť udalosť", +"Export" => "Exportovať", +"Eventinfo" => "Informácie o udalosti", +"Repeating" => "Opakovanie", +"Alarm" => "Alarm", +"Attendees" => "Účastníci", +"Share" => "Zdielať", +"Title of the Event" => "Nadpis udalosti", +"Category" => "Kategória", +"Separate categories with commas" => "Kategórie oddelené čiarkami", +"Edit categories" => "Úprava kategórií", +"Access Class" => "Prístupová trieda", +"All Day Event" => "Celodenná udalosť", +"From" => "Od", +"To" => "Do", +"Advanced options" => "Pokročilé možnosti", +"Location" => "Poloha", +"Location of the Event" => "Poloha udalosti", +"Description" => "Popis", +"Description of the Event" => "Popis udalosti", +"Repeat" => "Opakovať", +"Advanced" => "Pokročilé", +"Select weekdays" => "Do času", +"Select days" => "Vybrať dni", +"and the events day of year." => "a denné udalosti v roku.", +"and the events day of month." => "a denné udalosti v mesiaci.", +"Select months" => "Vybrať mesiace", +"Select weeks" => "Vybrať týždne", +"and the events week of year." => "a týždenné udalosti v roku.", +"Interval" => "Interval", +"End" => "Koniec", +"occurrences" => "výskyty", +"create a new calendar" => "vytvoriť nový kalendár", +"Import a calendar file" => "Importovať súbor kalendára", +"Please choose a calendar" => "Vybrať si kalendár", +"Name of new calendar" => "Meno nového kalendára", +"Take an available name!" => "Vybrať existujúce meno", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Kalendár s rovnakým názvom už existuje. Pokračovaním kalendáre spojíte.", +"Remove all events from the selected calendar" => "Odstrániť všetky udalosti z vybraného kalendára", +"Import" => "Importovať", +"Close Dialog" => "Zatvoriť dialóg", +"Create a new event" => "Vytvoriť udalosť", +"Share with:" => "Zdieľať s:", +"Shared with" => "Zdieľaný s:", +"Unshare" => "Nezdieľať", +"Nobody" => "Nikto", +"Shared via calendar" => "Zdieľané cez kalendár", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "POZNÁMKA: Činnosti na udalostiach zdieľaných cez kalendár budú mať vplyv na zdieľanie celého kalendára.", +"View an event" => "Zobraziť udalosť", +"No categories selected" => "Žiadne vybraté kategórie", +"of" => "z", +"at" => "v", +"General" => "Všeobecné", +"Timezone" => "Časová zóna", +"Update timezone automatically" => "Automaticky aktualizovať časovú zónu", +"Time format" => "Formát času", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Týždeň začína v ", +"Cache" => "Vyrovnávacia pamäť", +"Clear cache for repeating events" => "Vymazať vyrovnávaciu pamäť pre opakujúce sa udalosti", +"URLs" => "Odkazy URL", +"Calendar CalDAV syncing addresses" => "Kalendár CalDAV synchronizuje adresy", +"more info" => "viac informácií", +"Primary address (Kontact et al)" => "Primárna adresa", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Odkazy na iCalendar (iba na čítanie)" +); diff --git a/l10n/sl.php b/l10n/sl.php new file mode 100644 index 000000000..cde362926 --- /dev/null +++ b/l10n/sl.php @@ -0,0 +1,215 @@ + "Vsi koledarji niso povsem predpomnjeni", +"Everything seems to be completely cached" => "Izgleda, da je vse v predpomnilniku", +"No calendars found." => "Ni bilo najdenih koledarjev.", +"No events found." => "Ni bilo najdenih dogodkov.", +"Wrong calendar" => "Napačen koledar", +"You do not have the permissions to edit this event." => "Nimate dovoljenj za urejanje tega dogodka.", +"The file contained either no events or all events are already saved in your calendar." => "Datoteka ni vsebovala dogodkov ali pa so vsi dogodki že shranjeni v koledarju.", +"events has been saved in the new calendar" => "dogodki so bili shranjeni v nov koledar", +"Import failed" => "Uvoz je spodletel", +"events has been saved in your calendar" => "dogodki so bili shranjeni v vaš koledar", +"New Timezone:" => "Nov časovni pas:", +"Timezone changed" => "Časovni pas je spremenjen", +"Invalid request" => "Neveljavna zahteva", +"Calendar" => "Koledar", +"Deletion failed" => "Brisanje je spodletelo.", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "uporabnik", +"group" => "skupina", +"Editable" => "Možno urejanje", +"Shareable" => "Možna souporaba", +"Deletable" => "Možen izbris", +"ddd" => "ddd", +"ddd M/d" => "ddd, d. M.", +"dddd M/d" => "dddd, d. M.", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, d. MMM, yyyy", +"Sunday" => "nedelja", +"Monday" => "ponedeljek", +"Tuesday" => "torek", +"Wednesday" => "sreda", +"Thursday" => "četrtek", +"Friday" => "petek", +"Saturday" => "sobota", +"Sun." => "ned.", +"Mon." => "pon.", +"Tue." => "tor.", +"Wed." => "sre.", +"Thu." => "čet.", +"Fri." => "pet.", +"Sat." => "sob.", +"January" => "januar", +"February" => "februar", +"March" => "marec", +"April" => "april", +"May" => "maj", +"June" => "junij", +"July" => "julij", +"August" => "avgust", +"September" => "september", +"October" => "oktober", +"November" => "november", +"December" => "december", +"Jan." => "jan.", +"Feb." => "feb.", +"Mar." => "mar.", +"Apr." => "apr.", +"May." => "maj", +"Jun." => "jun.", +"Jul." => "jul.", +"Aug." => "avg.", +"Sep." => "sep.", +"Oct." => "okt.", +"Nov." => "nov.", +"Dec." => "dec.", +"All day" => "Cel dan", +"New Calendar" => "Nov koledar", +"Missing or invalid fields" => "Manjkajoča ali neveljavna polja", +"Title" => "Naslov", +"From Date" => "od Datum", +"From Time" => "od Čas", +"To Date" => "do Datum", +"To Time" => "do Čas", +"The event ends before it starts" => "Dogodek se konča preden se začne", +"There was a database fail" => "Napaka v podatkovni zbirki", +"Birthday" => "Rojstni dan", +"Business" => "Poslovno", +"Call" => "Pokliči", +"Clients" => "Stranke", +"Deliverer" => "Dobavitelj", +"Holidays" => "Dopust", +"Ideas" => "Ideje", +"Journey" => "Potovanje", +"Jubilee" => "Obletnica", +"Meeting" => "Sestanek", +"Other" => "Ostalo", +"Personal" => "Osebno", +"Projects" => "Projekt", +"Questions" => "Vprašanja", +"Work" => "Delo", +"by" => "od", +"unnamed" => "neimenovan", +"You do not have the permissions to update this calendar." => "Nimate dovoljenj za posodobitev tega koledarja.", +"You do not have the permissions to delete this calendar." => "Nimate dovoljenj za izbris tega koledarja.", +"You do not have the permissions to add to this calendar." => "Nimate dovoljenj za dodajanje tega koledarja.", +"You do not have the permissions to add events to this calendar." => "Nimate dovoljenj za dodajanje dogodkov v ta koledar.", +"You do not have the permissions to delete this event." => "Nimate dovoljenj za izbris tega dogodka.", +"Busy" => "Zaseden", +"Public" => "Javno", +"Private" => "Zasebno", +"Confidential" => "Zaupno", +"Does not repeat" => "Se ne ponavlja", +"Daily" => "Dnevno", +"Weekly" => "Tedensko", +"Every Weekday" => "Vsak dan v tednu", +"Bi-Weekly" => "Dvakrat tedensko", +"Monthly" => "Mesečno", +"Yearly" => "Letno", +"never" => "nikoli", +"by occurrences" => "po številu dogodkov", +"by date" => "po datumu", +"by monthday" => "po dnevu v mesecu", +"by weekday" => "po dnevu v tednu", +"events week of month" => "dogodki tedna v mesecu", +"first" => "prvi", +"second" => "drugi", +"third" => "tretji", +"fourth" => "četrti", +"fifth" => "peti", +"last" => "zadnji", +"by events date" => "po datumu dogodka", +"by yearday(s)" => "po številu let", +"by weeknumber(s)" => "po tednu v letu", +"by day and month" => "po dnevu in mesecu", +"Date" => "Datum", +"Cal." => "Kol.", +"Week" => "Teden", +"Month" => "Mesec", +"List" => "Seznam", +"Today" => "Danes", +"Settings" => "Nastavitve", +"Your calendars" => "Vaši koledarji", +"CalDav Link" => "CalDav povezava", +"Share Calendar" => "Daj koledar v souporabo", +"Download" => "Prejmi", +"Edit" => "Uredi", +"Delete" => "Izbriši", +"New calendar" => "Nov koledar", +"Edit calendar" => "Uredi koledar", +"Displayname" => "Ime za prikaz", +"Active" => "Dejavno", +"Calendar color" => "Barva koledarja", +"Save" => "Shrani", +"Submit" => "Potrdi", +"Cancel" => "Prekliči", +"Edit an event" => "Uredi dogodek", +"Export" => "Izvozi", +"Eventinfo" => "Informacije od dogodku", +"Repeating" => "Ponavljanja", +"Alarm" => "Alarm", +"Attendees" => "Udeleženci", +"Share" => "Souporaba", +"Title of the Event" => "Naslov dogodka", +"Category" => "Kategorija", +"Separate categories with commas" => "Kategorije ločite z vejico", +"Edit categories" => "Uredi kategorije", +"Access Class" => "Razred dostopa", +"All Day Event" => "Celodnevni dogodek", +"From" => "Od", +"To" => "Do", +"Advanced options" => "Napredne možnosti", +"Location" => "Mesto", +"Location of the Event" => "Mesto dogodka", +"Description" => "Opis", +"Description of the Event" => "Opis dogodka", +"Repeat" => "Ponovi", +"Advanced" => "Napredno", +"Select weekdays" => "Izberite dneve v tednu", +"Select days" => "Izberite dneve", +"and the events day of year." => "in dnevu dogodka v letu.", +"and the events day of month." => "in dnevu dogodka v mesecu.", +"Select months" => "Izberite mesece", +"Select weeks" => "Izberite tedne", +"and the events week of year." => "in tednu dogodka v letu.", +"Interval" => "Časovni razmik", +"End" => "Konec", +"occurrences" => "ponovitve", +"create a new calendar" => "Ustvari nov koledar", +"Import a calendar file" => "Uvozi datoteko koledarja", +"Please choose a calendar" => "Prosimo, če izberete koledar", +"Name of new calendar" => "Ime novega koledarja", +"Take an available name!" => "Izberite prosto ime!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Koledar s tem imenom že obstaja. Če nadaljujete, bosta koledarja združena.", +"Remove all events from the selected calendar" => "Odstrani vse dogodke iz izbranega koledarja", +"Import" => "Uvozi", +"Close Dialog" => "Zapri dialog", +"Create a new event" => "Ustvari nov dogodek", +"Share with:" => "Daj v souporabo z:", +"Shared with" => "V souporabi z", +"Unshare" => "Daj iz souporabe", +"Nobody" => "Nihče", +"Shared via calendar" => "V souporabi preko koledarja", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "OPOMBA: Dejanja na dogodkih v souporabi preko koledarja bodo vplivala na celotno souporabo koledarja.", +"View an event" => "Poglej dogodek", +"No categories selected" => "Nobena kategorija ni izbrana", +"of" => "od", +"at" => "pri", +"General" => "Splošno", +"Timezone" => "Časovni pas", +"Update timezone automatically" => "Samodejno posodobi časovni pas", +"Time format" => "Oblika zapisa časa", +"24h" => "24 ur", +"12h" => "12 ur", +"Start week on" => "Začni teden na", +"Cache" => "Predpomnilnik", +"Clear cache for repeating events" => "Počisti predpomnilnik za ponavljajoče dogodke", +"URLs" => "URLji", +"Calendar CalDAV syncing addresses" => "CalDAV naslov za usklajevanje koledarjev", +"more info" => "dodatne informacije", +"Primary address (Kontact et al)" => "Glavni naslov (Kontakt et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "iCalendar povezava/e samo za branje" +); diff --git a/l10n/sr.php b/l10n/sr.php new file mode 100644 index 000000000..120905948 --- /dev/null +++ b/l10n/sr.php @@ -0,0 +1,203 @@ + "Нису сви календари комплетно кеширани", +"Everything seems to be completely cached" => "Изгледа да је све комплетно кеширано", +"No calendars found." => "Није пронађен ниједан календар.", +"No events found." => "Није пронађен ниједан догађај.", +"Wrong calendar" => "Погрешан календар", +"You do not have the permissions to edit this event." => "Немате дозволу да мењате овај догађај", +"Import failed" => "Увоз није успео", +"events has been saved in your calendar" => "догађаји су сачувани у вашем календару", +"New Timezone:" => "Нова временска зона:", +"Timezone changed" => "Временска зона је промењена", +"Invalid request" => "Неисправан захтев", +"Calendar" => "Календар", +"Deletion failed" => "Брисање није успело", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ддд д MMMM[ гггг]{ - [ддд д] MMMM гггг}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ддд д MMMM[ гггг] ЧЧ:мм{ - [ ддд д MMMM гггг] ЧЧ:мм}", +"user" => "корисник", +"group" => "група", +"Editable" => "Променљиво", +"Shareable" => "Дељиво", +"Deletable" => "Избрисиво", +"ddd" => "ддд", +"ddd M/d" => "ддд М/д", +"dddd M/d" => "дддд M/д", +"MMMM yyyy" => "MMMM гггг", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM д[ гггг]{ '—'[ MMM] д гггг}", +"dddd, MMM d, yyyy" => "дддд, MMM д, гггг", +"Sunday" => "Недеља", +"Monday" => "Понедељак", +"Tuesday" => "Уторак", +"Wednesday" => "Среда", +"Thursday" => "Четвртак", +"Friday" => "Петак", +"Saturday" => "Субота", +"Sun." => "Нед", +"Mon." => "Пон", +"Tue." => "Уто", +"Wed." => "Сре", +"Thu." => "Чет", +"Fri." => "Пет", +"Sat." => "Суб", +"January" => "Јануар", +"February" => "Фебруар", +"March" => "Март", +"April" => "Април", +"May" => "Мај", +"June" => "Јун", +"July" => "Јул", +"August" => "Август", +"September" => "Септембар", +"October" => "Октобар", +"November" => "Новембар", +"December" => "Децембар", +"Jan." => "Јан", +"Feb." => "Феб", +"Mar." => "Мар", +"Apr." => "Апр", +"May." => "Мај.", +"Jun." => "Јун", +"Jul." => "Јул", +"Aug." => "Авг", +"Sep." => "Сеп", +"Oct." => "Окт", +"Nov." => "Нов", +"Dec." => "Дец", +"All day" => "Цео дан", +"New Calendar" => "Нови календар", +"Missing or invalid fields" => "Недостајућа или неисправна поља", +"Title" => "Наслов", +"From Date" => "Од датума", +"From Time" => "Од времена", +"To Date" => "До датума", +"To Time" => "До времена", +"The event ends before it starts" => "Догађај се завршава пре него што треба да почне", +"There was a database fail" => "Грешка у бази", +"Birthday" => "Рођендан", +"Business" => "Посао", +"Call" => "Позив", +"Clients" => "Клијенти", +"Deliverer" => "Достављач", +"Holidays" => "Празници", +"Ideas" => "Идеје", +"Journey" => "путовање", +"Jubilee" => "јубилеј", +"Meeting" => "Састанак", +"Other" => "Друго", +"Personal" => "Лично", +"Projects" => "Пројекти", +"Questions" => "Питања", +"Work" => "Посао", +"unnamed" => "безимени", +"You do not have the permissions to update this calendar." => "Немате дозволу да освежите овај календар", +"You do not have the permissions to delete this calendar." => "Немате дозволу да избришете овај календар", +"You do not have the permissions to add to this calendar." => "Немате дозволу да додате у овај календар", +"You do not have the permissions to add events to this calendar." => "Немате дозволу да додате догађаје у овај календар", +"You do not have the permissions to delete this event." => "Немате дозволу да избришете овај догађај", +"Busy" => "Заузето", +"Public" => "Јавно", +"Private" => "Приватно", +"Confidential" => "Тајно", +"Does not repeat" => "Не понавља се", +"Daily" => "дневно", +"Weekly" => "недељно", +"Every Weekday" => "сваког дана у недељи", +"Bi-Weekly" => "двонедељно", +"Monthly" => "месечно", +"Yearly" => "годишње", +"never" => "никада", +"by occurrences" => "по учестаности појављивања", +"by date" => "по датуму", +"by monthday" => "по месецу", +"by weekday" => "по викенду", +"first" => "први", +"second" => "други", +"third" => "трећи", +"fourth" => "четврти", +"fifth" => "пети", +"last" => "последњи", +"by events date" => "по датуму догађаја", +"by yearday(s)" => "по години(ама)", +"by weeknumber(s)" => "по недељи(ама)", +"by day and month" => "по дану и месецу", +"Date" => "Датум", +"Cal." => "Кал.", +"Week" => "Недеља", +"Month" => "Месец", +"List" => "Списак", +"Today" => "Данас", +"Settings" => "Подешавања", +"Your calendars" => "Ваши календари", +"CalDav Link" => "КалДав веза", +"Share Calendar" => "Подели календар", +"Download" => "Преузми", +"Edit" => "Уреди", +"Delete" => "Обриши", +"New calendar" => "Нови календар", +"Edit calendar" => "Уреди календар", +"Displayname" => "Приказано име", +"Active" => "Активан", +"Calendar color" => "Боја календара", +"Save" => "Сними", +"Submit" => "Пошаљи", +"Cancel" => "Откажи", +"Edit an event" => "Уреди догађај", +"Export" => "Извези", +"Eventinfo" => "Опис догађаја", +"Repeating" => "Понављање", +"Alarm" => "Аларм", +"Attendees" => "Гости", +"Share" => "Дељење", +"Title of the Event" => "Наслов догађаја", +"Category" => "Категорија", +"Separate categories with commas" => "Одвоји категорије са запетама", +"Edit categories" => "Измени категорије", +"Access Class" => "Класа приступа", +"All Day Event" => "Целодневни догађај", +"From" => "Од", +"To" => "До", +"Advanced options" => "Напредне опције", +"Location" => "Локација", +"Location of the Event" => "Локација догађаја", +"Description" => "Опис", +"Description of the Event" => "Опис догађаја", +"Repeat" => "Понављај", +"Advanced" => "Напредно", +"Select weekdays" => "Одабери радне дане", +"Select days" => "Одабери дане", +"Select months" => "Изаберите месеце", +"Select weeks" => "Изаберите недеље", +"Interval" => "Интервал", +"End" => "Крај", +"occurrences" => "понављања", +"create a new calendar" => "направи нови календар", +"Import a calendar file" => "Увези датотеку календара", +"Please choose a calendar" => "Молимо вас да изаберете календар", +"Name of new calendar" => "Назив новог календара", +"Take an available name!" => "Изаберите неко од могућих имена", +"Remove all events from the selected calendar" => "Избришите све догађаје из одабраног календара", +"Import" => "Увези", +"Close Dialog" => "Затвори дијалог", +"Create a new event" => "Направи нови догађај", +"Share with:" => "Подели са:", +"Shared with" => "Подељено са:", +"Unshare" => "Укини дељење", +"Nobody" => "Нико", +"Shared via calendar" => "Подељено преко календара", +"View an event" => "Погледај догађај", +"No categories selected" => "Ниједна категорија није изабрана", +"of" => "од", +"at" => "на", +"General" => "Опште", +"Timezone" => "Временска зона", +"Update timezone automatically" => "Аутоматски ажурирај временску зону", +"Time format" => "Облик времена", +"24h" => "24 часа", +"12h" => "12 часова", +"Start week on" => "Недеља почиње од", +"Cache" => "Кеш", +"Clear cache for repeating events" => "Избриши кеш за поновљене догађаје", +"URLs" => "УРЛ", +"more info" => "више информација", +"iOS/OS X" => "iOS/OS X" +); diff --git a/l10n/sr@latin.php b/l10n/sr@latin.php new file mode 100644 index 000000000..43aa2ddc3 --- /dev/null +++ b/l10n/sr@latin.php @@ -0,0 +1,99 @@ + "Pogrešan kalendar", +"Timezone changed" => "Vremenska zona je promenjena", +"Invalid request" => "Neispravan zahtev", +"Calendar" => "Kalendar", +"Sunday" => "Nedelja", +"Monday" => "Ponedeljak", +"Tuesday" => "Utorak", +"Wednesday" => "Sreda", +"Thursday" => "Četvrtak", +"Friday" => "Petak", +"Saturday" => "Subota", +"Sun." => "Ned", +"Mon." => "Pon", +"Tue." => "Uto", +"Wed." => "Sre", +"Thu." => "Čet", +"Fri." => "Pet", +"Sat." => "Sub", +"January" => "Januar", +"February" => "Februar", +"March" => "Mart", +"April" => "April", +"May" => "Maj", +"June" => "Jun", +"July" => "Jul", +"August" => "Avgust", +"September" => "Septembar", +"October" => "Oktobar", +"November" => "Novembar", +"December" => "Decembar", +"Jan." => "Jan", +"Feb." => "Feb", +"Mar." => "Mar", +"Apr." => "Apr", +"May." => "Maj.", +"Jun." => "Jun", +"Jul." => "Jul", +"Aug." => "Avg", +"Sep." => "Sep", +"Oct." => "Okt", +"Nov." => "Nov", +"Dec." => "Dec", +"All day" => "Ceo dan", +"New Calendar" => "Novi kalendar", +"Title" => "Naslov", +"Birthday" => "Rođendan", +"Business" => "Posao", +"Call" => "Poziv", +"Clients" => "Klijenti", +"Deliverer" => "Dostavljač", +"Holidays" => "Praznici", +"Ideas" => "Ideje", +"Journey" => "putovanje", +"Jubilee" => "jubilej", +"Meeting" => "Sastanak", +"Other" => "Drugo", +"Personal" => "Lično", +"Projects" => "Projekti", +"Questions" => "Pitanja", +"Work" => "Posao", +"Does not repeat" => "Ne ponavlja se", +"Daily" => "dnevno", +"Weekly" => "nedeljno", +"Every Weekday" => "svakog dana u nedelji", +"Bi-Weekly" => "dvonedeljno", +"Monthly" => "mesečno", +"Yearly" => "godišnje", +"Week" => "Nedelja", +"Month" => "Mesec", +"List" => "Spisak", +"Today" => "Danas", +"Settings" => "Podešavanja", +"CalDav Link" => "KalDav veza", +"Download" => "Preuzmi", +"Edit" => "Uredi", +"Delete" => "Obriši", +"New calendar" => "Novi kalendar", +"Edit calendar" => "Uredi kalendar", +"Displayname" => "Prikazanoime", +"Active" => "Aktivan", +"Calendar color" => "Boja kalendara", +"Save" => "Snimi", +"Submit" => "Pošalji", +"Cancel" => "Otkaži", +"Edit an event" => "Uredi događaj", +"Title of the Event" => "Naslov događaja", +"Category" => "Kategorija", +"All Day Event" => "Celodnevni događaj", +"From" => "Od", +"To" => "Do", +"Location" => "Lokacija", +"Location of the Event" => "Lokacija događaja", +"Description" => "Opis", +"Description of the Event" => "Opis događaja", +"Repeat" => "Ponavljaj", +"Create a new event" => "Napravi novi događaj", +"Timezone" => "Vremenska zona" +); diff --git a/l10n/sv.php b/l10n/sv.php new file mode 100644 index 000000000..b2399282b --- /dev/null +++ b/l10n/sv.php @@ -0,0 +1,215 @@ + "Alla kalendrar är inte fullständigt sparade i cache", +"Everything seems to be completely cached" => "Allt verkar vara fullständigt sparat i cache", +"No calendars found." => "Inga kalendrar funna", +"No events found." => "Inga händelser funna.", +"Wrong calendar" => "Fel kalender", +"You do not have the permissions to edit this event." => "Du har inte behörighet att ändra denna händelse.", +"The file contained either no events or all events are already saved in your calendar." => "Filen innehöll inga händelser eller så är alla händelser redan sparade i kalendern.", +"events has been saved in the new calendar" => "händelser har sparats i den nya kalendern", +"Import failed" => "Misslyckad import", +"events has been saved in your calendar" => "händelse har sparats i din kalender", +"New Timezone:" => "Ny tidszon:", +"Timezone changed" => "Tidszon ändrad", +"Invalid request" => "Ogiltig begäran", +"Calendar" => "Kalender", +"Deletion failed" => "Raderingen misslyckades", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "Användare", +"group" => "Grupp", +"Editable" => "Redigerbar", +"Shareable" => "Utdelningsbar", +"Deletable" => "Raderbar", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "ddd, MMM d, yyyy", +"Sunday" => "Söndag", +"Monday" => "Måndag", +"Tuesday" => "Tisdag", +"Wednesday" => "Onsdag", +"Thursday" => "Torsdag", +"Friday" => "Fredag", +"Saturday" => "Lördag", +"Sun." => "Sön.", +"Mon." => "Mån.", +"Tue." => "Tis.", +"Wed." => "Ons.", +"Thu." => "Tor.", +"Fri." => "Fre.", +"Sat." => "Lör.", +"January" => "Januari", +"February" => "Februari", +"March" => "Mars", +"April" => "April", +"May" => "Maj", +"June" => "Juni", +"July" => "Juli", +"August" => "Augusti", +"September" => "September", +"October" => "Oktober", +"November" => "November", +"December" => "December", +"Jan." => "Jan.", +"Feb." => "Feb.", +"Mar." => "Mar.", +"Apr." => "Apr.", +"May." => "Maj.", +"Jun." => "Jun.", +"Jul." => "Jul.", +"Aug." => "Aug.", +"Sep." => "Sep.", +"Oct." => "Okt.", +"Nov." => "Nov.", +"Dec." => "Dec.", +"All day" => "Hela dagen", +"New Calendar" => "Ny kalender", +"Missing or invalid fields" => "Saknade eller ogiltiga fält", +"Title" => "Rubrik", +"From Date" => "Från datum", +"From Time" => "Från tid", +"To Date" => "Till datum", +"To Time" => "Till tid", +"The event ends before it starts" => "Händelsen slutar innan den börjar", +"There was a database fail" => "Det blev ett databasfel", +"Birthday" => "Födelsedag", +"Business" => "Företag", +"Call" => "Ringa", +"Clients" => "Klienter", +"Deliverer" => "Leverantör", +"Holidays" => "Semester", +"Ideas" => "Idéer", +"Journey" => "Resa", +"Jubilee" => "Jubileum", +"Meeting" => "Möte", +"Other" => "Annat", +"Personal" => "Personlig", +"Projects" => "Projekt", +"Questions" => "Frågor", +"Work" => "Arbetet", +"by" => "av", +"unnamed" => "Namn saknas", +"You do not have the permissions to update this calendar." => "Du har inte behörighet att ändra denna kalender.", +"You do not have the permissions to delete this calendar." => "Du har inte behörighet att radera denna kalender.", +"You do not have the permissions to add to this calendar." => "Du har inte behörighet att lägga till denna kalender.", +"You do not have the permissions to add events to this calendar." => "Du har inte behörighet att lägga till händelser till denna kalender.", +"You do not have the permissions to delete this event." => "Du har inte behörighet att radera denna händelse.", +"Busy" => "Upptagen", +"Public" => "Publik", +"Private" => "Privat", +"Confidential" => "Konfidentiell", +"Does not repeat" => "Upprepas inte", +"Daily" => "Dagligen", +"Weekly" => "Varje vecka", +"Every Weekday" => "Varje vardag", +"Bi-Weekly" => "Varannan vecka", +"Monthly" => "Varje månad", +"Yearly" => "Årligen", +"never" => "aldrig", +"by occurrences" => "efter händelser", +"by date" => "efter datum", +"by monthday" => "efter dag i månaden", +"by weekday" => "efter veckodag", +"events week of month" => "händelse vecka av månad", +"first" => "första", +"second" => "andra", +"third" => "tredje", +"fourth" => "fjärde", +"fifth" => "femte", +"last" => "sist", +"by events date" => "efter händelsedatum", +"by yearday(s)" => "efter årsdag(ar)", +"by weeknumber(s)" => "efter veckonummer", +"by day and month" => "efter dag och månad", +"Date" => "Datum", +"Cal." => "Kal.", +"Week" => "Vecka", +"Month" => "Månad", +"List" => "Lista", +"Today" => "Idag", +"Settings" => "Inställningar", +"Your calendars" => "Dina kalendrar", +"CalDav Link" => "CalDAV-länk", +"Share Calendar" => "Dela kalender", +"Download" => "Ladda ner", +"Edit" => "Redigera", +"Delete" => "Radera", +"New calendar" => "Nya kalender", +"Edit calendar" => "Redigera kalender", +"Displayname" => "Visningsnamn", +"Active" => "Aktiv", +"Calendar color" => "Kalender-färg", +"Save" => "Spara", +"Submit" => "Lägg till", +"Cancel" => "Avbryt", +"Edit an event" => "Redigera en händelse", +"Export" => "Exportera", +"Eventinfo" => "Händelseinfo", +"Repeating" => "Repetera", +"Alarm" => "Alarm", +"Attendees" => "Deltagare", +"Share" => "Dela", +"Title of the Event" => "Rubrik för händelsen", +"Category" => "Kategori", +"Separate categories with commas" => "Separera kategorier med komman", +"Edit categories" => "Redigera kategorier", +"Access Class" => "Åtkomstklass", +"All Day Event" => "Hela dagen", +"From" => "Från", +"To" => "Till", +"Advanced options" => "Avancerade alternativ", +"Location" => "Plats", +"Location of the Event" => "Platsen för händelsen", +"Description" => "Beskrivning", +"Description of the Event" => "Beskrivning av händelse", +"Repeat" => "Upprepa", +"Advanced" => "Avancerad", +"Select weekdays" => "Välj veckodagar", +"Select days" => "Välj dagar", +"and the events day of year." => "och händelsedagen för året.", +"and the events day of month." => "och händelsedagen för månaden.", +"Select months" => "Välj månader", +"Select weeks" => "Välj veckor", +"and the events week of year." => "och händelsevecka för året.", +"Interval" => "Hur ofta", +"End" => "Slut", +"occurrences" => "Händelser", +"create a new calendar" => "skapa en ny kalender", +"Import a calendar file" => "Importera en kalenderfil", +"Please choose a calendar" => "Välj en kalender", +"Name of new calendar" => "Namn på ny kalender", +"Take an available name!" => "Ta ett ledigt namn!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "En kalender med detta namn finns redan. Om du fortsätter ändå så kommer dessa kalendrar att slås samman.", +"Remove all events from the selected calendar" => "Ta bort alla händelser från den valda kalendern", +"Import" => "Importera", +"Close Dialog" => "Stäng ", +"Create a new event" => "Skapa en ny händelse", +"Share with:" => "Dela med:", +"Shared with" => "Delad med", +"Unshare" => "Sluta dela", +"Nobody" => "Ingen", +"Shared via calendar" => "Delad via kalender", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOTERA: Åtgärder på händelser som delas via kalendern kommer att påverka hela kalendern delning.", +"View an event" => "Visa en händelse", +"No categories selected" => "Inga kategorier valda", +"of" => "av", +"at" => "på", +"General" => "Allmänt", +"Timezone" => "Tidszon", +"Update timezone automatically" => "Uppdatera tidzon automatiskt", +"Time format" => "Tidformat", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Starta veckan på", +"Cache" => "Cache", +"Clear cache for repeating events" => "Töm cache för upprepade händelser", +"URLs" => "URL:er", +"Calendar CalDAV syncing addresses" => "Kalender CalDAV synkroniserar adresser", +"more info" => "mer info", +"Primary address (Kontact et al)" => "Primary address (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Read only iCalendar link(s)" +); diff --git a/l10n/ta_LK.php b/l10n/ta_LK.php new file mode 100644 index 000000000..e4210c7fb --- /dev/null +++ b/l10n/ta_LK.php @@ -0,0 +1,210 @@ + "எல்லா நாட்காட்டிகளும் முற்றாக இடைமாற்றுநினைவகம் ஆகவில்லை", +"Everything seems to be completely cached" => "எல்லாம் முற்றாக இடைமாற்றுநினைவகம் ஆகியதாக தென்படுகிறது", +"No calendars found." => "நாட்காட்டிகள் எதுவும் கண்டறியப்படவில்லை", +"No events found." => "நிகழ்வுகள் எதுவும் கண்டறியப்படவில்லை", +"Wrong calendar" => "பிழையான நாட்காட்டி", +"You do not have the permissions to edit this event." => "இந்த நிகழ்வை தொகுப்பதற்கு உங்களுக்கு அனுமதி இல்லை ", +"The file contained either no events or all events are already saved in your calendar." => "கோப்பு உங்களுடைய நாட்காட்டியில் ஒரு நிகழ்வும் இல்லாததையோ அல்லது ஏற்கனவே எல்லா நிகழ்வும் சேமித்ததையோ கொண்டுள்ளது. ", +"events has been saved in the new calendar" => "புதிய நாட்காட்டியில் நிகழ்வுகள் சேமிக்கப்பட்டது", +"Import failed" => "இறக்குமதி தோல்வியுற்றது", +"events has been saved in your calendar" => "உங்களுடைய நாட்காட்டியில் நிகழ்வுகள் சேமிக்கப்பட்டது", +"New Timezone:" => "புதிய நேர மண்டலம்", +"Timezone changed" => "நேர மண்டலம் மாறியுள்ளது", +"Invalid request" => "செல்லுபடியற்ற வேண்டுகோள்", +"Calendar" => "நாட்காட்டி", +"Deletion failed" => "நீக்கம் தோல்வியடைந்தது", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "பயனாளர்", +"group" => "குழு", +"Editable" => "தொகுக்கக்கூடியது", +"Shareable" => "பகிரக்கூடியது", +"Deletable" => "அழிக்கக்கூடியது", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "ddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "ஞாயிற்றுக்கிழமை", +"Monday" => "திங்கட்கிழமை", +"Tuesday" => "செவ்வாய்க்கிழமை", +"Wednesday" => "புதன்கிழமை", +"Thursday" => "வியாழக்கிழமை", +"Friday" => "வெள்ளிக்கிழமை", +"Saturday" => "சனிக்கிழமை", +"Sun." => "ஞாயிறு", +"Mon." => "திங்கள்", +"Tue." => "செவ்வாய்", +"Wed." => "புதன்", +"Thu." => "வியாழன்", +"Fri." => "வெள்ளி", +"Sat." => "சனி", +"January" => "தை", +"February" => "மாசி", +"March" => "பங்குனி", +"April" => "சித்திரை", +"May" => "வைகாசி", +"June" => "ஆனி", +"July" => "ஆடி", +"August" => "ஆவணி", +"September" => "புரட்டாசி", +"October" => "ஐப்பசி", +"November" => "கார்த்திகை", +"December" => "மார்கழி", +"Jan." => "தை", +"Feb." => "மாசி", +"Mar." => "பங்குனி", +"Apr." => "சித்திரை", +"May." => "வைகாசி", +"Jun." => "ஆனி", +"Jul." => "ஆடி", +"Aug." => "ஆவணி", +"Sep." => "புரட்டாதி", +"Oct." => "ஐப்பசி", +"Nov." => "கார்த்திகை", +"Dec." => "மார்கழி", +"All day" => "அனைத்து நாட்களும்", +"New Calendar" => "புதிய நாட்காட்டி", +"Missing or invalid fields" => "விடுபட்ட அல்லது செல்லுபடியற்ற புலங்கள்", +"Title" => "தலைப்பு", +"From Date" => "திகதியிலிருந்து", +"From Time" => "நேரத்திலிருந்து", +"To Date" => "திகதி வரை", +"To Time" => "நேரம் வரை", +"The event ends before it starts" => "அது தொடங்குவதற்கு முன்பு நிகழ்வு முடிந்துவிடும்", +"There was a database fail" => "தரவுத்தள பிழை ஒன்று உள்ளது", +"Birthday" => "பிறந்த நாள்", +"Business" => "வியாபாரம்", +"Call" => "அழை", +"Clients" => "வாடிக்கையாளர்கள்", +"Deliverer" => "விநியோகிப்பவர்", +"Holidays" => "விடுமுறைகள்", +"Ideas" => "எண்ணங்கள்", +"Journey" => "பயணம்", +"Jubilee" => "50 ஆவது வருட விழா", +"Meeting" => "கூட்டம்", +"Other" => "மற்றவை", +"Personal" => "தனிப்பட்ட", +"Projects" => "செயல் திட்டங்கள்", +"Questions" => "கேள்விகள்", +"Work" => "வேலை", +"by" => "மூலம்", +"unnamed" => "பெயரிடப்படவில்லை", +"You do not have the permissions to update this calendar." => "இந்த நாட்காட்டியை இற்​றைப்படுத்துவதற்கு உங்களுக்கு அனுமதி இல்லை ", +"You do not have the permissions to delete this calendar." => "இந்த நாட்காட்டியை நீக்குவதற்கு உங்களுக்கு அனுமதி இல்லை ", +"You do not have the permissions to add to this calendar." => "இந்த நாட்காட்டியை சேர்ப்பதற்கு உங்களுக்கு அனுமதி இல்லை", +"You do not have the permissions to add events to this calendar." => "இந்த நாட்காட்டிக்கு நிகழ்வுகளை சேர்க்க உங்களுக்கு அனுமதி இல்லை", +"You do not have the permissions to delete this event." => "இந்த நிகழ்வை அழிப்பதற்கு உங்களுக்கு அனுமதி இல்லை ", +"Does not repeat" => "மீண்டும் இல்லாமல்", +"Daily" => "ஒவ்வொரு நாளும்", +"Weekly" => "வாரந்தோறும்", +"Every Weekday" => "ஒவ்வொரு வாரநாளும்", +"Bi-Weekly" => "இரு வாரத்திற்கு ஒரு முறை", +"Monthly" => "மாதந்தோரும்", +"Yearly" => "வருடந்தோறும்", +"never" => "ஒருபோதும்", +"by occurrences" => "நிகழ்வுகள் மூலம்", +"by date" => "திகதியினால்", +"by monthday" => "மாதநாளில்", +"by weekday" => "வாரநாளில்", +"events week of month" => "மாதத்தின் நிகழ்வு வாரங்கள்", +"first" => "முதலாவது", +"second" => "இரண்டாவது", +"third" => "மூன்றாவது", +"fourth" => "நான்காவது", +"fifth" => "ஐந்தாவது", +"last" => "இறுதி", +"by events date" => "நிகழ்வை கொண்ட திகதி", +"by yearday(s)" => "வருட நாள் (கள்) மூலம்", +"by weeknumber(s)" => "வார எண்ணிக்கை (கள்) மூலம்", +"by day and month" => "நாள் மற்றும் மாதம் மூலம்", +"Date" => "திகதி", +"Cal." => "Cal.", +"Week" => "வாரம்", +"Month" => "மாதம்", +"List" => "பட்டியல்", +"Today" => "இன்று", +"Settings" => "அமைப்புகள்", +"Your calendars" => "உங்களுடைய நாட்காட்டிகள்", +"CalDav Link" => "CalDav இணைப்பு", +"Share Calendar" => "நாட்காட்டியை பகிர்க", +"Download" => "பதிவிறக்குக", +"Edit" => "தொகுக்க", +"Delete" => "அழிக்க", +"New calendar" => "புதிய நாட்காட்டி", +"Edit calendar" => "நாட்காட்டியை தொகுக்க", +"Displayname" => "காட்சி பெயர்", +"Active" => "இயங்கும்", +"Calendar color" => "நாட்காட்டி நிறம்", +"Save" => "சேமிக்க", +"Submit" => "சமர்ப்பிக்குக", +"Cancel" => "இரத்து செய்க", +"Edit an event" => "ஒரு நிகழ்வை தொகுக்க", +"Export" => "ஏற்றுமதி", +"Eventinfo" => "நிகழ்வு தகவல்", +"Repeating" => "மீண்டும்", +"Alarm" => "எச்சரிக்கை", +"Attendees" => "பங்கேற்பாளர்கள்", +"Share" => "பகிர்வு", +"Title of the Event" => "நிகழ்வின் தலைப்பு", +"Category" => "வகை", +"Separate categories with commas" => "வகைகளை காற்புள்ளியினால் வேறுப்படுத்துக", +"Edit categories" => "வகைகளை தொகுக்க", +"All Day Event" => "அனைத்து நாள் நிகழ்வு", +"From" => "இருந்து ", +"To" => "இற்கு", +"Advanced options" => "மேலதிக தெரிவுகள்", +"Location" => "இடம்", +"Location of the Event" => "நிகழ்வு நடைபெறும் இடம்", +"Description" => "விவரணம்", +"Description of the Event" => "நிகழ்வு பற்றிய விவரணம்", +"Repeat" => "மீண்டும்", +"Advanced" => "உயர்ந்த", +"Select weekdays" => "வாரநாட்களை தெரிவுசெய்க", +"Select days" => "நாட்களை தெரிவுசெய்க", +"and the events day of year." => "வருடத்தில் நிகழ்வை கொண்ட நாட்கள்", +"and the events day of month." => "மாதத்தில் நிகழ்வை கொண்ட நாட்கள்", +"Select months" => "மாதங்களை தெரிவுசெய்க", +"Select weeks" => "வாரங்களை தெரிவுசெய்க", +"and the events week of year." => "வருடத்தில் நிகழ்வை கொண்ட வாரங்கள்", +"Interval" => "இடைவேளை", +"End" => "இறுதி", +"occurrences" => "நிகழ்ச்சி", +"create a new calendar" => "புதிய நாட்காட்டி ஒன்றை உருவாக்குக", +"Import a calendar file" => "நாட்காட்டி கோப்பொன்றை இறக்குமதி செய்க", +"Please choose a calendar" => "தயவுசெய்து ஒரு நாட்காட்டியை தெரிவுசெய்க", +"Name of new calendar" => "புதிய நாட்காட்டியின் பெயர்", +"Take an available name!" => "கிடைக்கக்கூடிய பெயரொன்றை எடுக்க!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "இந்த பெயரைக்கொண்ட நாட்காட்டி ஏற்கனவே உள்ளது. நீங்கள் இதனை தொடர்ந்தால், நாட்காட்டிகள் ஒன்றிணைக்கப்படலாம். ", +"Remove all events from the selected calendar" => "தெரிவுசெய்யப்பட்ட நாட்காட்டியில் உள்ள எல்லா நிகழ்வுகளையும் அகற்றுக ", +"Import" => "இறக்குமதி", +"Close Dialog" => "Dialog மூடுக", +"Create a new event" => "புதிய நிகழ்வை உருவாக்க", +"Share with:" => "இடையில் பகிர்க:", +"Shared with" => "உடன் பகிர்க", +"Unshare" => "பகிரமுடியாது", +"Nobody" => "ஒருவருமில்லை", +"Shared via calendar" => "நாட்காட்டியினூடாக பகிர்க", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "குறிப்பு: நாட்காட்டியினூடாக பகிரப்பட்ட நிகழ்வுகளின் செயல்கள் முழு நாட்காட்டி பகிர்வையும் பாதிக்கும். ", +"View an event" => "ஒரு நிகழ்வை பார்வையிடவும்", +"No categories selected" => "வகைகள் தெரிவுசெய்யப்படவில்லை", +"of" => "உடைய", +"at" => "இல்", +"General" => "பொதுவானது", +"Timezone" => "நேர மண்டலம்", +"Update timezone automatically" => "நேர மண்டலத்தை தன்னிச்சையாக இற்றைப்படுத்துக", +"Time format" => "நேர வடிவமைப்பு", +"24h" => "24ஆம்", +"12h" => "12ஆம்", +"Start week on" => "தொடங்கும் வாரம்", +"Cache" => "இடைமாற்றுநினைவகம்", +"Clear cache for repeating events" => "மீள் நிகழ்ச்சிக்கான இடைமாற்றுநினைவகத்தை சுத்தப்படுத்துக", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "CalDAV நாட்காட்டியின் ஒத்திசை முகவரிகள்", +"more info" => "மேலதிக தகவல்", +"Primary address (Kontact et al)" => "முதன்மை முகவரி (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "வாசிக்க மட்டும் கூடிய iCalendar இணைப்பு (கள்)" +); diff --git a/l10n/te.php b/l10n/te.php new file mode 100644 index 000000000..376d7a177 --- /dev/null +++ b/l10n/te.php @@ -0,0 +1,26 @@ + "ఆదివారం", +"Monday" => "సోమవారం", +"Tuesday" => "మంగళవారం", +"Wednesday" => "బుధవారం", +"Thursday" => "గురువారం", +"Friday" => "శుక్రవారం", +"Saturday" => "శనివారం", +"January" => "జనవరి", +"February" => "ఫిబ్రవరి", +"March" => "మార్చి", +"April" => "ఏప్రిల్", +"May" => "మే", +"June" => "జూన్", +"July" => "జూలై", +"August" => "ఆగస్ట్", +"September" => "సెప్టెంబర్", +"October" => "అక్టోబర్", +"November" => "నవంబర్", +"December" => "డిసెంబర్", +"List" => "జాబితా", +"Settings" => "అమరికలు", +"Delete" => "తొలగించు", +"Save" => "భద్రపరచు", +"Cancel" => "రద్దుచేయి" +); diff --git a/l10n/th_TH.php b/l10n/th_TH.php new file mode 100644 index 000000000..215582617 --- /dev/null +++ b/l10n/th_TH.php @@ -0,0 +1,215 @@ + "ไม่ใช่ปฏิทินทั้งหมดที่จะถูกจัดเก็บข้อมูลไว้ในหน่วยความจำแคชอย่างสมบูรณ์", +"Everything seems to be completely cached" => "ทุกสิ่งทุกอย่างได้ถูกเก็บเข้าไปไว้ในหน่วยความจำแคชอย่างสมบูรณ์แล้ว", +"No calendars found." => "ไม่พบปฏิทินที่ต้องการ", +"No events found." => "ไม่พบกิจกรรมที่ต้องการ", +"Wrong calendar" => "ปฏิทินไม่ถูกต้อง", +"You do not have the permissions to edit this event." => "คุณไม่ได้รับสิทธิ์ให้แก้ไขกิจกรรมนี้", +"The file contained either no events or all events are already saved in your calendar." => "ไฟล์ดังกล่าวบรรจุข้อมูลกิจกรรมที่มีอยู่แล้วในปฏิทินของคุณ", +"events has been saved in the new calendar" => "กิจกรรมได้ถูกบันทึกไปไว้ในปฏิทินที่สร้างขึ้นใหม่แล้ว", +"Import failed" => "การนำเข้าข้อมูลล้มเหลว", +"events has been saved in your calendar" => "กิจกรรมได้ถูกบันทึกเข้าไปไว้ในปฏิทินของคุณแล้ว", +"New Timezone:" => "สร้างโซนเวลาใหม่:", +"Timezone changed" => "โซนเวลาถูกเปลี่ยนแล้ว", +"Invalid request" => "คำร้องขอไม่ถูกต้อง", +"Calendar" => "ปฏิทิน", +"Deletion failed" => "การลบทิ้งล้มเหลว", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "ผู้ใช้งาน", +"group" => "กลุ่มผู้ใช้งาน", +"Editable" => "สามารถแก้ไขได้", +"Shareable" => "สามารถแชร์ข้อมูลได้", +"Deletable" => "สามารถลบทิ้งได้", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "วันอาทิตย์", +"Monday" => "วันจันทร์", +"Tuesday" => "วันอังคาร", +"Wednesday" => "วันพุธ", +"Thursday" => "วันพฤหัสบดี", +"Friday" => "วันศุกร์", +"Saturday" => "วันเสาร์", +"Sun." => "อา.", +"Mon." => "จ.", +"Tue." => "อ.", +"Wed." => "พ.", +"Thu." => "พฤ.", +"Fri." => "ศ.", +"Sat." => "ส.", +"January" => "มกราคม", +"February" => "กุมภาพันธ์", +"March" => "มีนาคม", +"April" => "เมษายน", +"May" => "พฤษภาคม", +"June" => "มิถุนายน", +"July" => "กรกฏาคม", +"August" => "สิงหาคม", +"September" => "กันยายน", +"October" => "ตุลาคม", +"November" => "พฤศจิกายน", +"December" => "ธันวาคม", +"Jan." => "ม.ค.", +"Feb." => "ก.พ.", +"Mar." => "มี.ค.", +"Apr." => "เม.ย.", +"May." => "พ.ค.", +"Jun." => "มิ.ย.", +"Jul." => "ก.ค.", +"Aug." => "ส.ค.", +"Sep." => "ก.ย.", +"Oct." => "ต.ค.", +"Nov." => "พ.ย.", +"Dec." => "ธ.ค.", +"All day" => "ทั้งวัน", +"New Calendar" => "สร้างปฏิทินใหม่", +"Missing or invalid fields" => "ช่องข้อมูลไม่ถูกต้องหรือเกิดการสูญหาย", +"Title" => "ชื่อกิจกรรม", +"From Date" => "จากวันที่", +"From Time" => "ตั้งแต่เวลา", +"To Date" => "ถึงวันที่", +"To Time" => "ถึงเวลา", +"The event ends before it starts" => "วันที่สิ้นสุดกิจกรรมดังกล่าวอยู่ก่อนวันเริ่มต้น", +"There was a database fail" => "เกิดความล้มเหลวกับฐานข้อมูล", +"Birthday" => "วันเกิด", +"Business" => "ธุรกิจ", +"Call" => "โทรติดต่อ", +"Clients" => "ลูกค้า", +"Deliverer" => "จัดส่ง", +"Holidays" => "วันหยุด", +"Ideas" => "ไอเดีย", +"Journey" => "การเดินทาง", +"Jubilee" => "งานเลี้ยง", +"Meeting" => "นัดประชุม", +"Other" => "อื่นๆ", +"Personal" => "ส่วนตัว", +"Projects" => "โครงการ", +"Questions" => "คำถาม", +"Work" => "งาน", +"by" => "โดย", +"unnamed" => "ไม่มีชื่อ", +"You do not have the permissions to update this calendar." => "คุณไม่ได้รับสิทธิ์ให้อัพเดทปฏิทินนี้", +"You do not have the permissions to delete this calendar." => "คุณไม่ได้รับสิทธิ์ให้ลบปฏิทินนี้ทิ้งไป", +"You do not have the permissions to add to this calendar." => "คุณไม่ได้รับสิทธิ์ให้เพิ่มปฏิทินนี้", +"You do not have the permissions to add events to this calendar." => "คุณไม่ได้รับสิทธิ์ให้เพิ่มกิจกรรมเข้าไปในปฏิทินนี้", +"You do not have the permissions to delete this event." => "คุณไม่ได้รับสิทธิ์ให้ลบกิจกรรมนี้", +"Busy" => "ไม่ว่าง", +"Public" => "สาธารณะ", +"Private" => "ส่วนตัว", +"Confidential" => "ความลับ", +"Does not repeat" => "ไม่ต้องทำซ้ำ", +"Daily" => "รายวัน", +"Weekly" => "รายสัปดาห์", +"Every Weekday" => "ทุกวันหยุด", +"Bi-Weekly" => "รายปักษ์", +"Monthly" => "รายเดือน", +"Yearly" => "รายปี", +"never" => "ไม่ต้องเลย", +"by occurrences" => "ตามจำนวนที่ปรากฏ", +"by date" => "ตามวันที่", +"by monthday" => "จากเดือน", +"by weekday" => "จากสัปดาห์", +"events week of month" => "สัปดาห์ที่มีกิจกรรมของเดือน", +"first" => "ลำดับแรก", +"second" => "ลำดับที่สอง", +"third" => "ลำดับที่สาม", +"fourth" => "ลำดับที่สี่", +"fifth" => "ลำดับที่ห้า", +"last" => "ลำดับสุดท้าย", +"by events date" => "ตามวันที่จัดกิจกรรม", +"by yearday(s)" => "ของเมื่อวานนี้", +"by weeknumber(s)" => "จากหมายเลขของสัปดาห์", +"by day and month" => "ตามวันและเดือน", +"Date" => "วันที่", +"Cal." => "คำนวณ", +"Week" => "สัปดาห์", +"Month" => "เดือน", +"List" => "รายการ", +"Today" => "วันนี้", +"Settings" => "ตั้งค่า", +"Your calendars" => "ปฏิทินของคุณ", +"CalDav Link" => "ลิงค์ CalDav", +"Share Calendar" => "เปิดแชร์ปฏิทิน", +"Download" => "ดาวน์โหลด", +"Edit" => "แก้ไข", +"Delete" => "ลบ", +"New calendar" => "สร้างปฏิทินใหม่", +"Edit calendar" => "แก้ไขปฏิทิน", +"Displayname" => "ชื่อที่ต้องการให้แสดง", +"Active" => "ใช้งาน", +"Calendar color" => "สีของปฏิทิน", +"Save" => "บันทึก", +"Submit" => "ส่งข้อมูล", +"Cancel" => "ยกเลิก", +"Edit an event" => "แก้ไขกิจกรรม", +"Export" => "ส่งออกข้อมูล", +"Eventinfo" => "ข้อมูลเกี่ยวกับกิจกรรม", +"Repeating" => "ทำซ้ำ", +"Alarm" => "แจ้งเตือน", +"Attendees" => "ผู้เข้าร่วมกิจกรรม", +"Share" => "แชร์", +"Title of the Event" => "ชื่อของกิจกรรม", +"Category" => "หมวดหมู่", +"Separate categories with commas" => "คั่นระหว่างรายการหมวดหมู่ด้วยเครื่องหมายจุลภาคหรือคอมม่า", +"Edit categories" => "แก้ไขหมวดหมู่", +"Access Class" => "เข้าเรียน", +"All Day Event" => "เป็นกิจกรรมตลอดทั้งวัน", +"From" => "จาก", +"To" => "ถึง", +"Advanced options" => "ตัวเลือกขั้นสูง", +"Location" => "สถานที่", +"Location of the Event" => "สถานที่จัดกิจกรรม", +"Description" => "คำอธิบาย", +"Description of the Event" => "คำอธิบายเกี่ยวกับกิจกรรม", +"Repeat" => "ทำซ้ำ", +"Advanced" => "ขั้นสูง", +"Select weekdays" => "เลือกสัปดาห์", +"Select days" => "เลือกวัน", +"and the events day of year." => "และวันที่มีเหตุการณ์เกิดขึ้นในปี", +"and the events day of month." => "และวันที่มีเหตุการณ์เกิดขึ้นในเดือน", +"Select months" => "เลือกเดือน", +"Select weeks" => "เลือกสัปดาห์", +"and the events week of year." => "และสัปดาห์ที่มีเหตุการณ์เกิดขึ้นในปี", +"Interval" => "ช่วงเวลา", +"End" => "สิ้นสุด", +"occurrences" => "จำนวนที่ปรากฏ", +"create a new calendar" => "สร้างปฏิทินใหม่", +"Import a calendar file" => "นำเข้าไฟล์ปฏิทิน", +"Please choose a calendar" => "กรุณาเลือกปฏิทิน", +"Name of new calendar" => "ชื่อของปฏิทิน", +"Take an available name!" => "เลือกชื่อที่ต้องการ", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "ปฏิทินชื่อดังกล่าวถูกใช้งานไปแล้ว หากคุณยังดำเนินการต่อไป ปฏิทินดังกล่าวนี้จะถูกผสานข้อมูลเข้าด้วยกัน", +"Remove all events from the selected calendar" => "ลบกิจกรรมทั้งหมดออกจากปฏิทินที่เลือกไว้", +"Import" => "นำเข้าข้อมูล", +"Close Dialog" => "ปิดกล่องข้อความโต้ตอบ", +"Create a new event" => "สร้างกิจกรรมใหม่", +"Share with:" => "แชร์ด้วย:", +"Shared with" => "แชร์ด้วย", +"Unshare" => "ยกเลิกการแชร์", +"Nobody" => "ไม่มีใคร", +"Shared via calendar" => "แชร์ผ่านทางปฏิทิน", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "หมายเหตุ: การกระทำหรือกิจกรรมที่ได้รับการแชรผ่านทางปฏิทินจะมีผลกระทบกับการแชร์ปฏิทินทั้งหมด", +"View an event" => "ดูกิจกรรม", +"No categories selected" => "ยังไม่ได้เลือกหมวดหมู่", +"of" => "ของ", +"at" => "ที่", +"General" => "ทั่วไป", +"Timezone" => "โซนเวลา", +"Update timezone automatically" => "อัพเดทโซนเวลาอัตโนมัติ", +"Time format" => "รูปแบบเวลา", +"24h" => "24 ช.ม.", +"12h" => "12 ช.ม.", +"Start week on" => "เริ่มต้นสัปดาห์ด้วย", +"Cache" => "หน่วยความจำแคช", +"Clear cache for repeating events" => "ล้างข้อมูลในหน่วยความจำแคชสำหรับกิจกรรมที่ซ้ำซ้อน", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "ที่อยู่ที่ใช้สำหรับเชื่อมข้อมูลปฏิทิน CalDAV", +"more info" => "ข้อมูลเพิ่มเติม", +"Primary address (Kontact et al)" => "ที่อยู่หลัก (Kontact et al)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "อ่านเฉพาะลิงก์ iCalendar เท่านั้น" +); diff --git a/l10n/tr.php b/l10n/tr.php new file mode 100644 index 000000000..75a2cb903 --- /dev/null +++ b/l10n/tr.php @@ -0,0 +1,215 @@ + "Bütün takvimler tamamen ön belleğe alınmadı", +"Everything seems to be completely cached" => "Bütün herşey tamamen ön belleğe alınmış görünüyor", +"No calendars found." => "Takvim yok.", +"No events found." => "Etkinlik yok.", +"Wrong calendar" => "Yanlış takvim", +"You do not have the permissions to edit this event." => "Bu olayı düzeltme yetkisine sahip değilsiniz.", +"The file contained either no events or all events are already saved in your calendar." => "Dosya ya hiçbir etkinlik içermiyor veya bütün etkinlikler takviminizde zaten saklı.", +"events has been saved in the new calendar" => "Etkinlikler yeni takvimde saklandı", +"Import failed" => "İçeri aktarma başarısız oldu.", +"events has been saved in your calendar" => "Etkinlikler takviminizde saklandı", +"New Timezone:" => "Yeni Zamandilimi:", +"Timezone changed" => "Zaman dilimi değiştirildi", +"Invalid request" => "Geçersiz istek", +"Calendar" => "Takvim", +"Deletion failed" => "Silme başarısız oldu", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ggg g aaaa[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ggg g aaaa[ yyyy]{ - [ddd d] MMMM yyyy}", +"user" => "kullanıcı", +"group" => "grup", +"Editable" => "Düzenlenebilir", +"Shareable" => "Paylaşılabilir", +"Deletable" => "Silinebilir", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "AAA g[ yyyy]{ '—'[ AAA] g yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Pazar", +"Monday" => "Pazartesi", +"Tuesday" => "Salı", +"Wednesday" => "Çarşamba", +"Thursday" => "Perşembe", +"Friday" => "Cuma", +"Saturday" => "Cumartesi", +"Sun." => "Paz.", +"Mon." => "Pzt.", +"Tue." => "Sal.", +"Wed." => "Çar.", +"Thu." => "Per.", +"Fri." => "Cum.", +"Sat." => "Cmt.", +"January" => "Ocak", +"February" => "Şubat", +"March" => "Mart", +"April" => "Nisan", +"May" => "Mayıs", +"June" => "Haziran", +"July" => "Temmuz", +"August" => "Ağustos", +"September" => "Eylül", +"October" => "Ekim", +"November" => "Kasım", +"December" => "Aralık", +"Jan." => "Oca.", +"Feb." => "Şbt.", +"Mar." => "Mar.", +"Apr." => "Nis", +"May." => "May.", +"Jun." => "Haz.", +"Jul." => "Tem.", +"Aug." => "Agu.", +"Sep." => "Eyl.", +"Oct." => "Eki.", +"Nov." => "Kas.", +"Dec." => "Ara.", +"All day" => "Tüm gün", +"New Calendar" => "Yeni Takvim", +"Missing or invalid fields" => "Eksik veya geçersiz alan", +"Title" => "Başlık", +"From Date" => "Bu Tarihten", +"From Time" => "Bu Saatten", +"To Date" => "Bu Tarihe", +"To Time" => "Bu Saate", +"The event ends before it starts" => "Olay başlamadan önce bitiyor", +"There was a database fail" => "Bir veritabanı başarısızlığı oluştu", +"Birthday" => "Doğum günü", +"Business" => "İş", +"Call" => "Arama", +"Clients" => "Müşteriler", +"Deliverer" => "Teslimatçı", +"Holidays" => "Tatil günleri", +"Ideas" => "Fikirler", +"Journey" => "Seyahat", +"Jubilee" => "Yıl dönümü", +"Meeting" => "Toplantı", +"Other" => "Diğer", +"Personal" => "Kişisel", +"Projects" => "Projeler", +"Questions" => "Sorular", +"Work" => "İş", +"by" => "hazırlayan", +"unnamed" => "isimsiz", +"You do not have the permissions to update this calendar." => "Bu takvimi güncelleme yetkisine sahip değilsiniz.", +"You do not have the permissions to delete this calendar." => "Bu takvimi silme yetkisine sahip değilsiniz.", +"You do not have the permissions to add to this calendar." => "Bu takvime ekleme yetkisine sahip değilsiniz.", +"You do not have the permissions to add events to this calendar." => "Bu takvime olay ekleme yetkisine sahip değilsiniz.", +"You do not have the permissions to delete this event." => "Bu olayı silme yetkisine sahip değilsiniz.", +"Busy" => "Meşgul", +"Public" => "Halka açık", +"Private" => "Özel", +"Confidential" => "Gizli", +"Does not repeat" => "Tekrar etmiyor", +"Daily" => "Günlük", +"Weekly" => "Haftalı", +"Every Weekday" => "Haftaiçi Her gün", +"Bi-Weekly" => "İki haftada bir", +"Monthly" => "Aylık", +"Yearly" => "Yıllı", +"never" => "asla", +"by occurrences" => "sıklığa göre", +"by date" => "tarihe göre", +"by monthday" => "ay günlerine göre", +"by weekday" => "hafta günlerine göre", +"events week of month" => "ayın etkinlikler haftası", +"first" => "birinci", +"second" => "ikinci", +"third" => "üçüncü", +"fourth" => "dördüncü", +"fifth" => "beşinci", +"last" => "sonuncu", +"by events date" => "olay tarihine göre", +"by yearday(s)" => "yıl gün(ler)ine göre", +"by weeknumber(s)" => "hafta sayı(lar)ına göre", +"by day and month" => "gün ve aya göre", +"Date" => "Tarih", +"Cal." => "Takv.", +"Week" => "Hafta", +"Month" => "Ay", +"List" => "Liste", +"Today" => "Bugün", +"Settings" => "Ayarlar", +"Your calendars" => "Takvimleriniz", +"CalDav Link" => "CalDav Bağlantısı", +"Share Calendar" => "Takvimi paylaş", +"Download" => "İndir", +"Edit" => "Düzenle", +"Delete" => "Sil", +"New calendar" => "Yeni takvim", +"Edit calendar" => "Takvimi düzenle", +"Displayname" => "Görünüm adı", +"Active" => "Aktif", +"Calendar color" => "Takvim rengi", +"Save" => "Kaydet", +"Submit" => "Gönder", +"Cancel" => "İptal", +"Edit an event" => "Bir olay düzenle", +"Export" => "Dışa aktar", +"Eventinfo" => "Etkinlik bilgisi", +"Repeating" => "Tekrarlama", +"Alarm" => "Alarm", +"Attendees" => "Katılanlar", +"Share" => "Paylaş", +"Title of the Event" => "Olayın Başlığı", +"Category" => "Kategori", +"Separate categories with commas" => "Kategorileri virgülle ayırın", +"Edit categories" => "Kategorileri düzenle", +"Access Class" => "Erişim Sınıfı", +"All Day Event" => "Tüm Gün Olay", +"From" => "Kimden", +"To" => "Kime", +"Advanced options" => "Gelişmiş opsiyonlar", +"Location" => "Konum", +"Location of the Event" => "Olayın Konumu", +"Description" => "Açıklama", +"Description of the Event" => "Olayın Açıklaması", +"Repeat" => "Tekrar", +"Advanced" => "Gelişmiş", +"Select weekdays" => "Hafta günlerini seçin", +"Select days" => "Günleri seçin", +"and the events day of year." => "ve yılın etkinlikler günü.", +"and the events day of month." => "ve ayın etkinlikler günü.", +"Select months" => "Ayları seç", +"Select weeks" => "Haftaları seç", +"and the events week of year." => "ve yılın etkinkinlikler haftası.", +"Interval" => "Aralık", +"End" => "Son", +"occurrences" => "olaylar", +"create a new calendar" => "Yeni bir takvim oluştur", +"Import a calendar file" => "Takvim dosyasını içeri aktar", +"Please choose a calendar" => "Lütfen takvim seçiniz", +"Name of new calendar" => "Yeni takvimin adı", +"Take an available name!" => "Müsait ismi al !", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Bu isimde bir takvim zaten mevcut. Yine de devam ederseniz bu takvimler birleştirilecektir.", +"Remove all events from the selected calendar" => "Seçilen bir takvimden girişlerı kaldır", +"Import" => "İçe Al", +"Close Dialog" => "Diyalogu kapat", +"Create a new event" => "Yeni olay oluştur", +"Share with:" => "Paylaş:", +"Shared with" => "Paylaş", +"Unshare" => "Paylaşılmayan", +"Nobody" => "Hiçkimse", +"Shared via calendar" => "Takvimle paylaş", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "NOT: Takvimde paylaşılan etkinlikler üzerindeki eylemler tüm takvim paylaşımını etkileyecektir.", +"View an event" => "Bir olay görüntüle", +"No categories selected" => "Kategori seçilmedi", +"of" => "nın", +"at" => "üzerinde", +"General" => "Genel", +"Timezone" => "Zaman dilimi", +"Update timezone automatically" => "Otomatik saatdilimi güncelle", +"Time format" => "Saat biçimi", +"24h" => "24s", +"12h" => "12s", +"Start week on" => "Haftabaşı", +"Cache" => "Önbellek", +"Clear cache for repeating events" => "Tekrar eden etkinlikler için ön belleği temizle.", +"URLs" => "URL'ler", +"Calendar CalDAV syncing addresses" => "CalDAV takvimi adresleri senkronize ediyor.", +"more info" => "daha fazla bilgi", +"Primary address (Kontact et al)" => "Öncelikli adres", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Sadece okunabilir iCalendar link(ler)i" +); diff --git a/l10n/uk.php b/l10n/uk.php new file mode 100644 index 000000000..40aa7ea2e --- /dev/null +++ b/l10n/uk.php @@ -0,0 +1,215 @@ + "Не всі календарі повністю закешовано", +"Everything seems to be completely cached" => "Все, начебто, закешовано повністю", +"No calendars found." => "Календарів не знадено.", +"No events found." => "Подій не знайдено.", +"Wrong calendar" => "Невірний календар", +"You do not have the permissions to edit this event." => "У вас немає прав редагувати цю подію.", +"The file contained either no events or all events are already saved in your calendar." => "Файл або не містить подій, або всі події вже збережені у вашому календарі.", +"events has been saved in the new calendar" => "подій було збережено в новому календарі", +"Import failed" => "Імпорт не був виконаний", +"events has been saved in your calendar" => "подій було збережено у вашому календарі", +"New Timezone:" => "Новий часовий пояс", +"Timezone changed" => "Часовий пояс змінено", +"Invalid request" => "Некоректний запит", +"Calendar" => "Календар", +"Deletion failed" => "Видалення не було виконано", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "користувач", +"group" => "група", +"Editable" => "Редагуєме", +"Shareable" => "Розподіляєме", +"Deletable" => "Видаляєме", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Неділя", +"Monday" => "Понеділок", +"Tuesday" => "Вівторок", +"Wednesday" => "Середа", +"Thursday" => "Четвер", +"Friday" => "П'ятниця", +"Saturday" => "Субота", +"Sun." => "Нед.", +"Mon." => "Пн.", +"Tue." => "Вт.", +"Wed." => "Ср.", +"Thu." => "Чт.", +"Fri." => "Пт.", +"Sat." => "Сб.", +"January" => "Січень", +"February" => "Лютий", +"March" => "Березень", +"April" => "Квітень", +"May" => "Травень", +"June" => "Червень", +"July" => "Липень", +"August" => "Серпень", +"September" => "Вересень", +"October" => "Жовтень", +"November" => "Листопад", +"December" => "Грудень", +"Jan." => "Січ.", +"Feb." => "Лют.", +"Mar." => "Бер.", +"Apr." => "Кві.", +"May." => "Тра.", +"Jun." => "Чер.", +"Jul." => "Лип.", +"Aug." => "Сер.", +"Sep." => "Вер.", +"Oct." => "Жов.", +"Nov." => "Лис.", +"Dec." => "Гру.", +"All day" => "Увесь день", +"New Calendar" => "Новий Календар", +"Missing or invalid fields" => "Відсутні або невірні поля", +"Title" => "Назва", +"From Date" => "Від Дати", +"From Time" => "З Часу", +"To Date" => "До Часу", +"To Time" => "По Дату", +"The event ends before it starts" => "Подія завершається до її початку", +"There was a database fail" => "Сталася помилка бази даних", +"Birthday" => "День народження", +"Business" => "Справи", +"Call" => "Зателефонувати", +"Clients" => "Клієнти", +"Deliverer" => "Постачальник", +"Holidays" => "Свята", +"Ideas" => "Ідеї", +"Journey" => "Поїздка", +"Jubilee" => "Ювілей", +"Meeting" => "Зустріч", +"Other" => "Інше", +"Personal" => "Особисте", +"Projects" => "Проекти", +"Questions" => "Запитання", +"Work" => "Робота", +"by" => "по", +"unnamed" => "неназваний", +"You do not have the permissions to update this calendar." => "У вас немає прав оновлювати цей календар.", +"You do not have the permissions to delete this calendar." => "У вас немає прав видаляти цей календар.", +"You do not have the permissions to add to this calendar." => "У вас немає прав додавати у цей календар.", +"You do not have the permissions to add events to this calendar." => "У вас немає прав додавати події у цей календар.", +"You do not have the permissions to delete this event." => "У вас немає прав видалити цю подію.", +"Busy" => "Зайнято", +"Public" => "Загально", +"Private" => "Приватно", +"Confidential" => "Конфіденційно", +"Does not repeat" => "Не повторювати", +"Daily" => "Щоденно", +"Weekly" => "Щотижня", +"Every Weekday" => "По будням", +"Bi-Weekly" => "Кожні дві неділі", +"Monthly" => "Щомісяця", +"Yearly" => "Щорічно", +"never" => "ніколи", +"by occurrences" => "по нагодах", +"by date" => "по датах", +"by monthday" => "по днях місяця", +"by weekday" => "по днях тижня", +"events week of month" => "подій тижня місяця", +"first" => "перший", +"second" => "другий", +"third" => "третій", +"fourth" => "четвертий", +"fifth" => "п'ятий", +"last" => "останній", +"by events date" => "по датах подій", +"by yearday(s)" => "по днях року", +"by weeknumber(s)" => "по номеру тижня", +"by day and month" => "по дню та місяцю", +"Date" => "Дата", +"Cal." => "Кал.", +"Week" => "Тиждень", +"Month" => "Місяць", +"List" => "Список", +"Today" => "Сьогодні", +"Settings" => "Налаштування", +"Your calendars" => "Ваші календарі", +"CalDav Link" => "CalDav З'єднання", +"Share Calendar" => "Розподілити Календар", +"Download" => "Завантажити", +"Edit" => "Редагувати", +"Delete" => "Видалити", +"New calendar" => "Новий календар", +"Edit calendar" => "Редагувати календар", +"Displayname" => "Відображуване ім'я", +"Active" => "Активний", +"Calendar color" => "Колір календаря", +"Save" => "Зберегти", +"Submit" => "Передати", +"Cancel" => "Відмінити", +"Edit an event" => "Редагувати подію", +"Export" => "Експорт", +"Eventinfo" => "Інформація про подію", +"Repeating" => "Повторення", +"Alarm" => "Тривога", +"Attendees" => "Учасників", +"Share" => "Поділитися", +"Title of the Event" => "Назва події", +"Category" => "Категорія", +"Separate categories with commas" => "Розділити категорії комами", +"Edit categories" => "Редагувати категорії", +"Access Class" => "Клас Доступу", +"All Day Event" => "Подія на весь день", +"From" => "З", +"To" => "По", +"Advanced options" => "Додаткові опції", +"Location" => "Місце", +"Location of the Event" => "Місце події", +"Description" => "Опис", +"Description of the Event" => "Опис події", +"Repeat" => "Повторювати", +"Advanced" => "Додатково", +"Select weekdays" => "Вибрати дні тижня", +"Select days" => "Вибрати дні", +"and the events day of year." => "і день подій в році.", +"and the events day of month." => "і день подій в місяці.", +"Select months" => "Вибрати місяці", +"Select weeks" => "Вибрати тижні", +"and the events week of year." => "і тиждень подій в році.", +"Interval" => "Інтервал", +"End" => "Кінець", +"occurrences" => "нагоди", +"create a new calendar" => "створити новий календар", +"Import a calendar file" => "Імпортувати файл календаря", +"Please choose a calendar" => "Будь ласка, оберіть календар", +"Name of new calendar" => "Назва нового календаря", +"Take an available name!" => "Візьміть доступне ім'я!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Календар з такою назвою вже існує. Якщо ви продовжите, ці календарі будуть поєднані.", +"Remove all events from the selected calendar" => "Видаліть всі події з обраного календаря", +"Import" => "Імпорт", +"Close Dialog" => "Закрити Діалог", +"Create a new event" => "Створити нову подію", +"Share with:" => "Поділитися з:", +"Shared with" => "Розподілено з", +"Unshare" => "Закрити доступ", +"Nobody" => "Ніхто", +"Shared via calendar" => "Розподілено за допомогою календаря", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "ПРИМІТКА: Дії з подіями, які викладені в загальний доступ в календарі, будуть впливати на загальну доступність календаря.", +"View an event" => "Подивитися подію", +"No categories selected" => "Жодної категорії не вибрано", +"of" => "з", +"at" => "в", +"General" => "Загалом", +"Timezone" => "Часовий пояс", +"Update timezone automatically" => "Оновити часовий пояс автоматично", +"Time format" => "Формат часу", +"24h" => "24Рі", +"12h" => "12Рі", +"Start week on" => "Тиждень починається з", +"Cache" => "Кеш", +"Clear cache for repeating events" => "Чистити кеш для подій, що повторюються", +"URLs" => "URL-ів", +"Calendar CalDAV syncing addresses" => "Адреси синхронізації CalDAV Календаря", +"more info" => "більше інформації", +"Primary address (Kontact et al)" => "Початкова адреса", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Читати лише iCalendar з'єднання" +); diff --git a/l10n/ur_PK.php b/l10n/ur_PK.php new file mode 100644 index 000000000..9c785e338 --- /dev/null +++ b/l10n/ur_PK.php @@ -0,0 +1,7 @@ + "ذاتی", +"Settings" => "سیٹینگز", +"Cancel" => "منسوخ کریں", +"Edit categories" => "زمرہ جات کی تدوین کریں", +"Advanced" => "ایڈوانسڈ" +); diff --git a/l10n/vi.php b/l10n/vi.php new file mode 100644 index 000000000..0544916a1 --- /dev/null +++ b/l10n/vi.php @@ -0,0 +1,215 @@ + "Không phải tất cả các lịch là hoàn toàn được lưu trong bộ nhớ cache", +"Everything seems to be completely cached" => "Mọi thứ dường như là hoàn toàn được lưu trong bộ nhớ cache", +"No calendars found." => "Không tìm thấy lịch.", +"No events found." => "Không tìm thấy sự kiện nào", +"Wrong calendar" => "Sai lịch", +"You do not have the permissions to edit this event." => "Bạn không có quyền chỉnh sửa sự kiện này.", +"The file contained either no events or all events are already saved in your calendar." => "Các tập tin có hay không có sự kiện hoặc tất cả các sự kiện đã được lưu trong lịch của bạn.", +"events has been saved in the new calendar" => "sự kiện đã được lưu vào lịch mới.", +"Import failed" => "Nhập vào thất bại", +"events has been saved in your calendar" => "sự kiện đã dược lưu vào lịch", +"New Timezone:" => "Múi giờ mới :", +"Timezone changed" => "Thay đổi múi giờ", +"Invalid request" => "Yêu cầu không hợp lệ", +"Calendar" => "Lịch", +"Deletion failed" => "Xóa thất bại", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "người dùng", +"group" => "nhóm", +"Editable" => "Có thể chỉnh sửa", +"Shareable" => "Có thể chia sẽ", +"Deletable" => "Có thể xóa", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "Chủ nhật", +"Monday" => "Thứ 2", +"Tuesday" => "Thứ 3", +"Wednesday" => "Thứ 4", +"Thursday" => "Thứ 5", +"Friday" => "Thứ ", +"Saturday" => "Thứ 7", +"Sun." => "Chủ nhật", +"Mon." => "Thứ hai", +"Tue." => "Thứ ba", +"Wed." => "Thứ tư", +"Thu." => "Thứ năm", +"Fri." => "Thứ sáu", +"Sat." => "Thứ bảy", +"January" => "Tháng 1", +"February" => "Tháng 2", +"March" => "Tháng 3", +"April" => "Tháng 4", +"May" => "Tháng 5", +"June" => "Tháng 6", +"July" => "Tháng 7", +"August" => "Tháng 8", +"September" => "Tháng 9", +"October" => "Tháng 10", +"November" => "Tháng 11", +"December" => "Tháng 12", +"Jan." => "Tháng 1", +"Feb." => "Tháng 2", +"Mar." => "Tháng 3", +"Apr." => "Tháng 4", +"May." => "Tháng 5", +"Jun." => "Tháng 6", +"Jul." => "Tháng 7", +"Aug." => "Tháng 8", +"Sep." => "Tháng 9", +"Oct." => "Tháng 10", +"Nov." => "Tháng 11", +"Dec." => "Tháng 12", +"All day" => "Tất cả các ngày", +"New Calendar" => "Lịch mới", +"Missing or invalid fields" => "Thông tin thiếu hoặc không hợp lệ", +"Title" => "Tiêu đề", +"From Date" => "Từ ngày", +"From Time" => "Từ thời gian", +"To Date" => "Tới ngày", +"To Time" => "Tới thời gian", +"The event ends before it starts" => "Sự kiện này kết thúc trước khi nó bắt đầu", +"There was a database fail" => "Lỗi truy xuất cơ sở dữ liệu", +"Birthday" => "Ngày sinh nhật", +"Business" => "Công việc", +"Call" => "Số điện thoại", +"Clients" => "Máy trạm", +"Deliverer" => "Giao", +"Holidays" => "Ngày lễ", +"Ideas" => "Ý tưởng", +"Journey" => "Cuộc hành trình", +"Jubilee" => "Lễ kỷ niệm", +"Meeting" => "Hội nghị", +"Other" => "Khác", +"Personal" => "Cá nhân", +"Projects" => "Dự án", +"Questions" => "Câu hỏi", +"Work" => "Công việc", +"by" => "bởi", +"unnamed" => "Không tên", +"You do not have the permissions to update this calendar." => "Bạn không có quyền cập nhật lịch này", +"You do not have the permissions to delete this calendar." => "Bạn không có quyền xóa lịch này", +"You do not have the permissions to add to this calendar." => "Bạn không có quyền thêm lịch này", +"You do not have the permissions to add events to this calendar." => "Bạn không có quyền thêm các sự kiện vào lịch này.", +"You do not have the permissions to delete this event." => "Bạn không có quyền xóa sự kiện này.", +"Busy" => "Bận", +"Public" => "Công khai", +"Private" => "Riêng tư", +"Confidential" => "Mật", +"Does not repeat" => "Không lặp lại", +"Daily" => "Hàng ngày", +"Weekly" => "Hàng tuần", +"Every Weekday" => "Mỗi ngày trong tuần", +"Bi-Weekly" => "Hai tuần một lần", +"Monthly" => "Hàng tháng", +"Yearly" => "Hàng năm", +"never" => "không thay đổi", +"by occurrences" => "bởi sự kiện", +"by date" => "bởi ngày", +"by monthday" => "bởi ngày trong tháng", +"by weekday" => "bởi ngày trong tuần", +"events week of month" => "sự kiện trong tuần của tháng", +"first" => "đầu tiên", +"second" => "Thứ hai", +"third" => "Thứ ba", +"fourth" => "Thứ tư", +"fifth" => "Thứ năm", +"last" => "sau", +"by events date" => "Theo ngày tháng sự kiện", +"by yearday(s)" => "Theo ngày trong năm", +"by weeknumber(s)" => "Theo số tuần", +"by day and month" => "Theo ngày, tháng", +"Date" => "Ngày", +"Cal." => "Cal.", +"Week" => "Tuần", +"Month" => "Tháng", +"List" => "Danh sách", +"Today" => "Hôm nay", +"Settings" => "Tùy chỉnh", +"Your calendars" => "Lịch của bạn", +"CalDav Link" => "Liên kết CalDav ", +"Share Calendar" => "Chia sẻ lịch", +"Download" => "Tải về", +"Edit" => "Chỉnh sửa", +"Delete" => "Xóa", +"New calendar" => "Lịch mới", +"Edit calendar" => "sửa Lịch", +"Displayname" => "Hiển thị tên", +"Active" => "Kích hoạt", +"Calendar color" => "Màu lịch", +"Save" => "Lưu", +"Submit" => "Xác nhận", +"Cancel" => "Hủy", +"Edit an event" => "Sửa sự kiện", +"Export" => "Xuất ra", +"Eventinfo" => "Thông tin sự kiện", +"Repeating" => "Lặp", +"Alarm" => "Nhắc nhở", +"Attendees" => "Người tham gia", +"Share" => "Chia sẻ", +"Title of the Event" => "Tên sự kiện", +"Category" => "Danh mục", +"Separate categories with commas" => "Phân cách bởi dấu phẩy", +"Edit categories" => "Sửa chuyên mục", +"Access Class" => "Mức truy cập", +"All Day Event" => "Sự kiện trong ngày", +"From" => "Từ", +"To" => "Tới", +"Advanced options" => "Tùy chọn nâng cao", +"Location" => "Nơi", +"Location of the Event" => "Nơi tổ chức sự kiện", +"Description" => "Mô tả", +"Description of the Event" => "Mô tả sự kiện", +"Repeat" => "Lặp lại", +"Advanced" => "Nâng cao", +"Select weekdays" => "Chọn ngày trong tuần", +"Select days" => "Chọn ngày", +"and the events day of year." => "và sự kiện của ngày trong năm", +"and the events day of month." => "và sự kiện của một ngày trong năm", +"Select months" => "Chọn tháng", +"Select weeks" => "Chọn tuần", +"and the events week of year." => "và sự kiện của tuần trong năm.", +"Interval" => "Khoảng từ", +"End" => "Kết thúc", +"occurrences" => "Sự kiện", +"create a new calendar" => "Tạo lịch mới", +"Import a calendar file" => "Nhập lịch từ tập tin", +"Please choose a calendar" => "Vui lòng chọn lịch", +"Name of new calendar" => "Tên lịch mới", +"Take an available name!" => "Chọn một tên tồn tại", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "Đã tồn tại một lịch với tên này. Nếu bạn tiếp tục, những lịch này sẽ trộn với nhau.", +"Remove all events from the selected calendar" => "Xóa tất cả những sự kiện đã chọn trong calendar", +"Import" => "Nhập vào", +"Close Dialog" => "Đóng hộp thoại", +"Create a new event" => "Tạo một sự kiện mới", +"Share with:" => "Chia sẻ với:", +"Shared with" => "Đã chia sẻ với", +"Unshare" => "Bỏ chia sẻ", +"Nobody" => "Không ai cả", +"Shared via calendar" => "Được chia sẻ qua lịch", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "Chú ý: Thao tác trên các sự kiện chia sẻ qua lịch sẽ ảnh hưởng đến việc chia sẻ toàn bộ lịch.", +"View an event" => "Xem một sự kiện", +"No categories selected" => "Không danh sách nào được chọn", +"of" => "của", +"at" => "tại", +"General" => "Tổng hợp", +"Timezone" => "Múi giờ", +"Update timezone automatically" => "Tự động cập nhập múi giờ", +"Time format" => "Định dạng thời gian", +"24h" => "24h", +"12h" => "12h", +"Start week on" => "Ngày bắt đầu trong tuần", +"Cache" => "Bộ nhớ đệm", +"Clear cache for repeating events" => "Xóa bộ nhớ đệm cho các sự kiện lặp đi lặp lại", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "Đồng bộ địa chỉ lịch CalDAV", +"more info" => "thông tin thêm", +"Primary address (Kontact et al)" => "Địa chỉ chính (Kontact et al) ", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "Chỉ đọc iCalendar link(s)" +); diff --git a/l10n/xgettextfiles b/l10n/xgettextfiles new file mode 100644 index 000000000..a8c260104 --- /dev/null +++ b/l10n/xgettextfiles @@ -0,0 +1,12 @@ +../appinfo/app.php +../lib/object.php +../templates/calendar.php +../templates/part.choosecalendar.php +../templates/part.choosecalendar.rowfields.php +../templates/part.editcalendar.php +../templates/part.editevent.php +../templates/part.eventform.php +../templates/part.import.php +../templates/part.newevent.php +../templates/settings.php +../templates/lAfix.php \ No newline at end of file diff --git a/l10n/zh_CN.GB2312.php b/l10n/zh_CN.GB2312.php new file mode 100644 index 000000000..4cad953fc --- /dev/null +++ b/l10n/zh_CN.GB2312.php @@ -0,0 +1,215 @@ + "日程表未全部缓存完成", +"Everything seems to be completely cached" => "日程表全部缓存完成", +"No calendars found." => "未找到日程表。", +"No events found." => "未找到事件。", +"Wrong calendar" => "错误的日历", +"You do not have the permissions to edit this event." => "您没有权限编辑此事件。", +"The file contained either no events or all events are already saved in your calendar." => "文件没有包含事件,或者所有事件已经存储在您的日程表中了。", +"events has been saved in the new calendar" => "事件已保存到新日程表", +"Import failed" => "导入失败", +"events has been saved in your calendar" => "事件已保存在您的日程表", +"New Timezone:" => "新时区", +"Timezone changed" => "时区改变了", +"Invalid request" => "非法请求", +"Calendar" => "日历", +"Deletion failed" => "删除失败", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "用户", +"group" => "群组", +"Editable" => "可编辑", +"Shareable" => "可分享", +"Deletable" => "可删除", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "星期天", +"Monday" => "星期一", +"Tuesday" => "星期二", +"Wednesday" => "星期三", +"Thursday" => "星期四", +"Friday" => "星期五", +"Saturday" => "星期六", +"Sun." => "星期日", +"Mon." => "星期一", +"Tue." => "星期二", +"Wed." => "星期三", +"Thu." => "星期四", +"Fri." => "星期五", +"Sat." => "星期六", +"January" => "一月", +"February" => "二月", +"March" => "三月", +"April" => "四月", +"May" => "五月", +"June" => "六月", +"July" => "七月", +"August" => "八月", +"September" => "九月", +"October" => "十月", +"November" => "十一月", +"December" => "十二月", +"Jan." => "一月", +"Feb." => "二月", +"Mar." => "三月", +"Apr." => "四月", +"May." => "五月", +"Jun." => "六月", +"Jul." => "七月", +"Aug." => "八月", +"Sep." => "九月", +"Oct." => "十月", +"Nov." => "十一月", +"Dec." => "十二月", +"All day" => "整天", +"New Calendar" => "新的日历", +"Missing or invalid fields" => "字段缺失或无效", +"Title" => "标题", +"From Date" => "从日期", +"From Time" => "从时间", +"To Date" => "到日期", +"To Time" => "到时间", +"The event ends before it starts" => "在它开始前需要结束的事件", +"There was a database fail" => "发生了一个数据库失败", +"Birthday" => "生日", +"Business" => "商务", +"Call" => "呼叫", +"Clients" => "客户端", +"Deliverer" => "交付者", +"Holidays" => "假期", +"Ideas" => "灵感", +"Journey" => "旅行", +"Jubilee" => "五十年纪念", +"Meeting" => "会面", +"Other" => "其它", +"Personal" => "个人的", +"Projects" => "项目", +"Questions" => "问题", +"Work" => "工作", +"by" => "由", +"unnamed" => "未命名", +"You do not have the permissions to update this calendar." => "您没有权限更新此日程表。", +"You do not have the permissions to delete this calendar." => "您没有权限删除此日程表。", +"You do not have the permissions to add to this calendar." => "您没有权限添加到此日程表。", +"You do not have the permissions to add events to this calendar." => "您没有权限向此日程表添加事件。", +"You do not have the permissions to delete this event." => "您没有权限删除此事件。", +"Busy" => "忙", +"Public" => "共享", +"Private" => "私人", +"Confidential" => "保密", +"Does not repeat" => "不要重复", +"Daily" => "每天", +"Weekly" => "每星期", +"Every Weekday" => "每个周末", +"Bi-Weekly" => "每两周", +"Monthly" => "每个月", +"Yearly" => "每年", +"never" => "从不", +"by occurrences" => "根据发生时", +"by date" => "根据日期", +"by monthday" => "根据月天", +"by weekday" => "根据星期", +"events week of month" => "时间每月发生的周数", +"first" => "首先", +"second" => "其次", +"third" => "第三", +"fourth" => "第四", +"fifth" => "第五", +"last" => "最后", +"by events date" => "根据时间日期", +"by yearday(s)" => "根据年数", +"by weeknumber(s)" => "根据周数", +"by day and month" => "根据天和月", +"Date" => "日期", +"Cal." => "Cal.", +"Week" => "星期", +"Month" => "月", +"List" => "列表", +"Today" => "今天", +"Settings" => "设置", +"Your calendars" => "您的日程表", +"CalDav Link" => "CalDav 链接", +"Share Calendar" => "分享日程表", +"Download" => "下载", +"Edit" => "编辑", +"Delete" => "删除", +"New calendar" => "新的日历", +"Edit calendar" => "编辑日历", +"Displayname" => "显示名称", +"Active" => "活动", +"Calendar color" => "日历颜色", +"Save" => "保存", +"Submit" => "提交", +"Cancel" => " 取消", +"Edit an event" => "编辑一个事件", +"Export" => "导出", +"Eventinfo" => "事件信息", +"Repeating" => "重复", +"Alarm" => "提醒", +"Attendees" => "参与人", +"Share" => "分享", +"Title of the Event" => "事件的标题", +"Category" => "分类", +"Separate categories with commas" => "用逗号分隔分类", +"Edit categories" => "编辑分类", +"Access Class" => "访问规则", +"All Day Event" => "每天的事件", +"From" => "从", +"To" => "到", +"Advanced options" => "进阶选项", +"Location" => "地点", +"Location of the Event" => "事件的地点", +"Description" => "解释", +"Description of the Event" => "事件描述", +"Repeat" => "重复", +"Advanced" => "进阶", +"Select weekdays" => "选择星期", +"Select days" => "选择日", +"and the events day of year." => "选择每年时间发生天数", +"and the events day of month." => "选择每个月事件发生的天", +"Select months" => "选择月份", +"Select weeks" => "选择星期", +"and the events week of year." => "每年时间发生的星期", +"Interval" => "间隔", +"End" => "结束", +"occurrences" => "发生", +"create a new calendar" => "创建一个新日程表", +"Import a calendar file" => "导入一个日程表文件", +"Please choose a calendar" => "请选择一个日程表", +"Name of new calendar" => "新日程表名称", +"Take an available name!" => "使用可用名称!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "同名日程表已存在。如果您强制继续,这些日程表将被合并。", +"Remove all events from the selected calendar" => "从所选日程表移除所有事件", +"Import" => "导入", +"Close Dialog" => "关闭对话框", +"Create a new event" => "新建一个时间", +"Share with:" => "分享给:", +"Shared with" => "分享给", +"Unshare" => "不共享", +"Nobody" => "没有人", +"Shared via calendar" => "已通过日程表共享", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "注意:操作通过日程表分享的事件将影响所有共享者的日程表。", +"View an event" => "查看事件", +"No categories selected" => "未选中分类", +"of" => "的", +"at" => "于", +"General" => "常规", +"Timezone" => "时区", +"Update timezone automatically" => "自动更新时区", +"Time format" => "时间格式", +"24h" => "24小时", +"12h" => "12小时", +"Start week on" => "一周开始于", +"Cache" => "缓存", +"Clear cache for repeating events" => "重复事件的缓存", +"URLs" => "网址", +"Calendar CalDAV syncing addresses" => "日程表 CaIDAV 同步地址", +"more info" => "更多信息", +"Primary address (Kontact et al)" => "主要地址 (Kontact 等)", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "只读 iCalendar 链接" +); diff --git a/l10n/zh_CN.php b/l10n/zh_CN.php new file mode 100644 index 000000000..e3b17b9bc --- /dev/null +++ b/l10n/zh_CN.php @@ -0,0 +1,215 @@ + "日程表未全部缓存完成", +"Everything seems to be completely cached" => "貌似所有事务已被缓存", +"No calendars found." => "无法找到日历。", +"No events found." => "无法找到事件。", +"Wrong calendar" => "错误的日历", +"You do not have the permissions to edit this event." => "您没有权限编辑此日历", +"The file contained either no events or all events are already saved in your calendar." => "文件中不含事件,或者所有事件已保存到日历中。", +"events has been saved in the new calendar" => "事件已被保存到新日历", +"Import failed" => "导入失败", +"events has been saved in your calendar" => "事件已被保存到您的日历", +"New Timezone:" => "新时区:", +"Timezone changed" => "时区已修改", +"Invalid request" => "非法请求", +"Calendar" => "日历", +"Deletion failed" => "删除失败", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "用户", +"group" => "组", +"Editable" => "可编辑", +"Shareable" => "可共享", +"Deletable" => "可删除", +"ddd" => "ddd", +"ddd M/d" => "ddd M/d", +"dddd M/d" => "dddd M/d", +"MMMM yyyy" => "MMMM yyyy", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "dddd, MMM d, yyyy", +"Sunday" => "星期日", +"Monday" => "星期一", +"Tuesday" => "星期二", +"Wednesday" => "星期三", +"Thursday" => "星期四", +"Friday" => "星期五", +"Saturday" => "星期六", +"Sun." => "星期天", +"Mon." => "星期一", +"Tue." => "星期二", +"Wed." => "星期三", +"Thu." => "星期四", +"Fri." => "星期五", +"Sat." => "星期六", +"January" => "一月", +"February" => "二月", +"March" => "三月", +"April" => "四月", +"May" => "五月", +"June" => "六月", +"July" => "七月", +"August" => "八月", +"September" => "九月", +"October" => "十月", +"November" => "十一月", +"December" => "十二月", +"Jan." => "一月", +"Feb." => "二月", +"Mar." => "三月", +"Apr." => "四月", +"May." => "五月", +"Jun." => "六月", +"Jul." => "七月", +"Aug." => "八月", +"Sep." => "九月", +"Oct." => "十月", +"Nov." => "十一月", +"Dec." => "十二月", +"All day" => "全天", +"New Calendar" => "新日历", +"Missing or invalid fields" => "缺少或无效的字段", +"Title" => "标题", +"From Date" => "从", +"From Time" => "从", +"To Date" => "至", +"To Time" => "至", +"The event ends before it starts" => "事件在开始前已结束", +"There was a database fail" => "数据库访问失败", +"Birthday" => "生日", +"Business" => "商务", +"Call" => "呼叫", +"Clients" => "客户", +"Deliverer" => "供应商", +"Holidays" => "节日", +"Ideas" => "想法", +"Journey" => "旅行", +"Jubilee" => "周年纪念", +"Meeting" => "会议", +"Other" => "其他", +"Personal" => "个人", +"Projects" => "项目", +"Questions" => "问题", +"Work" => "工作", +"by" => "被", +"unnamed" => "未命名", +"You do not have the permissions to update this calendar." => "您没有权限更新此日历", +"You do not have the permissions to delete this calendar." => "您没有权限删除此日历", +"You do not have the permissions to add to this calendar." => "您没有权限增加到此日历", +"You do not have the permissions to add events to this calendar." => "您没有权限增加事项到此日历", +"You do not have the permissions to delete this event." => "您没有权限删除此事件", +"Busy" => "忙碌", +"Public" => "公共", +"Private" => "私人", +"Confidential" => "证书", +"Does not repeat" => "不重复", +"Daily" => "每天", +"Weekly" => "每周", +"Every Weekday" => "每个工作日", +"Bi-Weekly" => "每两周", +"Monthly" => "每月", +"Yearly" => "每年", +"never" => "从不", +"by occurrences" => "按发生次数", +"by date" => "按日期", +"by monthday" => "按月的某天", +"by weekday" => "按星期的某天", +"events week of month" => "事件在每月的第几个星期", +"first" => "第一", +"second" => "第二", +"third" => "第三", +"fourth" => "第四", +"fifth" => "第五", +"last" => "最后", +"by events date" => "按事件日期", +"by yearday(s)" => "按每年的某天", +"by weeknumber(s)" => "按星期数", +"by day and month" => "按天和月份", +"Date" => "日期", +"Cal." => "日历", +"Week" => "星期", +"Month" => "月", +"List" => "列表", +"Today" => "今天", +"Settings" => "设置", +"Your calendars" => "您的日历", +"CalDav Link" => "CalDav 链接", +"Share Calendar" => "共享日历", +"Download" => "下载", +"Edit" => "编辑", +"Delete" => "删除", +"New calendar" => "新日历", +"Edit calendar" => "编辑日历", +"Displayname" => "显示名称", +"Active" => "活动", +"Calendar color" => "日历颜色", +"Save" => "保存", +"Submit" => "提交", +"Cancel" => "取消", +"Edit an event" => "编辑事件", +"Export" => "导出", +"Eventinfo" => "事件信息", +"Repeating" => "重复", +"Alarm" => "提醒", +"Attendees" => "参加者", +"Share" => "共享", +"Title of the Event" => "事件标题", +"Category" => "分类", +"Separate categories with commas" => "用逗号分隔分类", +"Edit categories" => "编辑分类", +"Access Class" => "访问分类", +"All Day Event" => "全天事件", +"From" => "自", +"To" => "至", +"Advanced options" => "高级选项", +"Location" => "地点", +"Location of the Event" => "事件地点", +"Description" => "描述", +"Description of the Event" => "事件描述", +"Repeat" => "重复", +"Advanced" => "高级", +"Select weekdays" => "选择星期中的某天", +"Select days" => "选择某天", +"and the events day of year." => "选择每年事件发生的日子", +"and the events day of month." => "选择每月事件发生的日子", +"Select months" => "选择月份", +"Select weeks" => "选择星期", +"and the events week of year." => "选择每年的事件发生的星期", +"Interval" => "间隔", +"End" => "结束", +"occurrences" => "次", +"create a new calendar" => "创建新日历", +"Import a calendar file" => "导入日历文件", +"Please choose a calendar" => "请选择日历", +"Name of new calendar" => "新日历名称", +"Take an available name!" => "使用有效名称!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "已有日历使用此名称。如果您执意继续,这些日历将会合并。", +"Remove all events from the selected calendar" => "从选定的日历中删除所有事件", +"Import" => "导入", +"Close Dialog" => "关闭对话框", +"Create a new event" => "创建新事件", +"Share with:" => "共享人", +"Shared with" => "已共享给", +"Unshare" => "取消共享", +"Nobody" => "没有人", +"Shared via calendar" => "通过日历分享", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "备注:处理通过日历共享的事件将会影响整个日历共享项", +"View an event" => "查看事件", +"No categories selected" => "无选中分类", +"of" => "在", +"at" => "在", +"General" => "常规", +"Timezone" => "时区", +"Update timezone automatically" => "自动更新时区", +"Time format" => "时间格式", +"24h" => "24小时", +"12h" => "12小时", +"Start week on" => "一周开始于", +"Cache" => "缓存", +"Clear cache for repeating events" => "清理重复事件的缓存", +"URLs" => "网址", +"Calendar CalDAV syncing addresses" => "日历 CalDAV 同步地址", +"more info" => "更多信息", +"Primary address (Kontact et al)" => "主地址", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "只读 iCanlendar 连接" +); diff --git a/l10n/zh_HK.php b/l10n/zh_HK.php new file mode 100644 index 000000000..7a231f3d5 --- /dev/null +++ b/l10n/zh_HK.php @@ -0,0 +1,40 @@ + "日曆", +"Sunday" => "星期日", +"Monday" => "星期一", +"Tuesday" => "星期二", +"Wednesday" => "星期三", +"Thursday" => "星期四", +"Friday" => "星期五", +"Saturday" => "星期六", +"January" => "一月", +"February" => "二月", +"March" => "三月", +"April" => "四月", +"May" => "五月", +"June" => "六月", +"July" => "七月", +"August" => "八月", +"September" => "九月", +"October" => "十月", +"November" => "十一月", +"December" => "十二月", +"Title" => "標題", +"Birthday" => "生日", +"Personal" => "個人", +"Work" => "工作", +"Settings" => "設定", +"Download" => "下載", +"Edit" => "編輯", +"Delete" => "刪除", +"Save" => "儲存", +"Cancel" => "取消", +"Share" => "分享", +"Advanced" => "進階", +"Import" => "導入", +"Unshare" => "取消分享", +"General" => "一般", +"more info" => "更多", +"Primary address (Kontact et al)" => "主要地址", +"iOS/OS X" => "iOS/OSX" +); diff --git a/l10n/zh_TW.php b/l10n/zh_TW.php new file mode 100644 index 000000000..7e54547ef --- /dev/null +++ b/l10n/zh_TW.php @@ -0,0 +1,215 @@ + "並非所有的日曆都完整 cached", +"Everything seems to be completely cached" => "似乎所有事都已完整 cached", +"No calendars found." => "沒有找到行事曆", +"No events found." => "沒有找到活動", +"Wrong calendar" => "錯誤日曆", +"You do not have the permissions to edit this event." => "您沒有權限來編輯此事件.", +"The file contained either no events or all events are already saved in your calendar." => "檔案中包含的所有事件或無事件都已存到您的日曆", +"events has been saved in the new calendar" => "事件已存到新日曆", +"Import failed" => "匯入失敗", +"events has been saved in your calendar" => "事件已存到您的日曆", +"New Timezone:" => "新時區:", +"Timezone changed" => "時區已變更", +"Invalid request" => "無效請求", +"Calendar" => "日曆", +"Deletion failed" => "移除失敗", +"ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}" => "ddd d MMMM[ yyyy]{ - [ddd d] MMMM yyyy}", +"ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}" => "ddd d MMMM[ yyyy] HH:mm{ - [ ddd d MMMM yyyy] HH:mm}", +"user" => "使用者", +"group" => "群組", +"Editable" => "可編輯", +"Shareable" => "可共享", +"Deletable" => "可刪除", +"ddd" => "ddd", +"ddd M/d" => "M/d ddd", +"dddd M/d" => "M/d dddd", +"MMMM yyyy" => "yyyy MMMM", +"MMM d[ yyyy]{ '—'[ MMM] d yyyy}" => "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", +"dddd, MMM d, yyyy" => "MMM d, dddd, yyyy", +"Sunday" => "週日", +"Monday" => "週一", +"Tuesday" => "週二", +"Wednesday" => "週三", +"Thursday" => "週四", +"Friday" => "週五", +"Saturday" => "週六", +"Sun." => "日", +"Mon." => "一", +"Tue." => "二", +"Wed." => "三", +"Thu." => "四", +"Fri." => "五", +"Sat." => "六", +"January" => "一月", +"February" => "二月", +"March" => "三月", +"April" => "四月", +"May" => "五月", +"June" => "六月", +"July" => "七月", +"August" => "八月", +"September" => "九月", +"October" => "十月", +"November" => "十一月", +"December" => "十二月", +"Jan." => "一月", +"Feb." => "二月", +"Mar." => "三月", +"Apr." => "四月", +"May." => "五月", +"Jun." => "六月", +"Jul." => "七月", +"Aug." => "八月", +"Sep." => "九月", +"Oct." => "十月", +"Nov." => "十一月", +"Dec." => "十二月", +"All day" => "整天", +"New Calendar" => "新日曆", +"Missing or invalid fields" => "欄位忘了填寫或無效的資料", +"Title" => "標題", +"From Date" => "自日期", +"From Time" => "至時間", +"To Date" => "至日期", +"To Time" => "至時間", +"The event ends before it starts" => "事件的結束在開始之前", +"There was a database fail" => "資料庫錯誤", +"Birthday" => "生日", +"Business" => "商業", +"Call" => "呼叫", +"Clients" => "客戶", +"Deliverer" => "遞送者", +"Holidays" => "節日", +"Ideas" => "主意", +"Journey" => "旅行", +"Jubilee" => "周年慶", +"Meeting" => "會議", +"Other" => "其他", +"Personal" => "個人", +"Projects" => "計畫", +"Questions" => "問題", +"Work" => "工作", +"by" => "由", +"unnamed" => "無名稱的", +"You do not have the permissions to update this calendar." => "您沒有權限來更新此日曆.", +"You do not have the permissions to delete this calendar." => "您沒有權限來刪除此日曆.", +"You do not have the permissions to add to this calendar." => "您沒有權限來新增此日曆.", +"You do not have the permissions to add events to this calendar." => "您沒有權限新增事件至此日曆.", +"You do not have the permissions to delete this event." => "您沒有權限來刪除此事件.", +"Busy" => "忙碌", +"Public" => "公開", +"Private" => "私人", +"Confidential" => "私密", +"Does not repeat" => "不重覆", +"Daily" => "每日", +"Weekly" => "每週", +"Every Weekday" => "每週末", +"Bi-Weekly" => "每雙週", +"Monthly" => "每月", +"Yearly" => "每年", +"never" => "絕不", +"by occurrences" => "由事件", +"by date" => "由日期", +"by monthday" => "依月份日期", +"by weekday" => "由平日", +"events week of month" => "月份中活動週", +"first" => "第一", +"second" => "第二", +"third" => "第三", +"fourth" => "第四", +"fifth" => "第五", +"last" => "最後", +"by events date" => "由事件日期", +"by yearday(s)" => "依年份日期", +"by weeknumber(s)" => "由週數", +"by day and month" => "由日與月", +"Date" => "日期", +"Cal." => "行事曆", +"Week" => "週", +"Month" => "月", +"List" => "清單", +"Today" => "今日", +"Settings" => "設定", +"Your calendars" => "你的行事曆", +"CalDav Link" => "CalDav 聯結", +"Share Calendar" => "分享行事曆", +"Download" => "下載", +"Edit" => "編輯", +"Delete" => "刪除", +"New calendar" => "新日曆", +"Edit calendar" => "編輯日曆", +"Displayname" => "顯示名稱", +"Active" => "作用中", +"Calendar color" => "日曆顏色", +"Save" => "儲存", +"Submit" => "提出", +"Cancel" => "取消", +"Edit an event" => "編輯事件", +"Export" => "匯出", +"Eventinfo" => "活動資訊", +"Repeating" => "重覆中", +"Alarm" => "鬧鐘", +"Attendees" => "出席者", +"Share" => "分享", +"Title of the Event" => "事件標題", +"Category" => "分類", +"Separate categories with commas" => "用逗點分隔分類", +"Edit categories" => "編輯分類", +"Access Class" => "存取群組", +"All Day Event" => "全天事件", +"From" => "自", +"To" => "至", +"Advanced options" => "進階選項", +"Location" => "位置", +"Location of the Event" => "事件位置", +"Description" => "描述", +"Description of the Event" => "事件描述", +"Repeat" => "重覆", +"Advanced" => "進階", +"Select weekdays" => "選擇平日", +"Select days" => "選擇日", +"and the events day of year." => "以及年中的活動日", +"and the events day of month." => "以及月中的活動日", +"Select months" => "選擇月", +"Select weeks" => "選擇週", +"and the events week of year." => "以及年中的活動週", +"Interval" => "間隔", +"End" => "結束", +"occurrences" => "事件", +"create a new calendar" => "建立新日曆", +"Import a calendar file" => "匯入日曆檔案", +"Please choose a calendar" => "請選擇一個日曆", +"Name of new calendar" => "新日曆名稱", +"Take an available name!" => "請取個可行的名稱!", +"A Calendar with this name already exists. If you continue anyhow, these calendars will be merged." => "已存在一個具有相同名稱的日曆。如果您依然要繼續,這些日曆將會合併成一個.", +"Remove all events from the selected calendar" => "把選擇中的日曆所有的活動移除", +"Import" => "匯入", +"Close Dialog" => "關閉對話", +"Create a new event" => "建立一個新事件", +"Share with:" => "分享給:", +"Shared with" => "共享者", +"Unshare" => "取消共享", +"Nobody" => "無", +"Shared via calendar" => "透過日曆分享", +"NOTE: Actions on events shared via calendar will affect the entire calendar sharing." => "注意: 透過日曆分享事件的動作將會影響整個分享日曆", +"View an event" => "觀看一個活動", +"No categories selected" => "沒有選擇分類", +"of" => "於", +"at" => "於", +"General" => "一般", +"Timezone" => "時區", +"Update timezone automatically" => "自動更新時區", +"Time format" => "時間格式", +"24h" => "24小時制", +"12h" => "12小時制", +"Start week on" => "一週起始於", +"Cache" => "Cache", +"Clear cache for repeating events" => "清除重複事件的 cache", +"URLs" => "URLs", +"Calendar CalDAV syncing addresses" => "CalDAV 的日曆同步地址", +"more info" => "更多資訊", +"Primary address (Kontact et al)" => "主要地址", +"iOS/OS X" => "iOS/OS X", +"Read only iCalendar link(s)" => "唯讀的 iCalendar 連結(們)" +); diff --git a/lib/app.php b/lib/app.php new file mode 100644 index 000000000..0c81cffb6 --- /dev/null +++ b/lib/app.php @@ -0,0 +1,222 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar; + +use \OCP\AppFramework\IAppContainer; + +use \OCA\Calendar\PublicAPI\Calendar; +use \OCA\Calendar\PublicAPI\Object; +use \OCA\Calendar\PublicAPI\Event; +use \OCA\Calendar\PublicAPI\Journal; +use \OCA\Calendar\PublicAPI\Todo; + +use \OCA\Calendar\BusinessLayer\BackendBusinessLayer; +use \OCA\Calendar\BusinessLayer\CalendarBusinessLayer; +use \OCA\Calendar\BusinessLayer\ObjectBusinessLayer; + +use \OCA\Calendar\Controller\BackendController; +use \OCA\Calendar\Controller\CalendarController; +use \OCA\Calendar\Controller\ObjectController; +use \OCA\Calendar\Controller\EventController; +use \OCA\Calendar\Controller\JournalController; +use \OCA\Calendar\Controller\TodoController; +use \OCA\Calendar\Controller\SettingsController; +use \OCA\Calendar\Controller\ViewController; + +use \OCA\Calendar\Db\BackendMapper; +use \OCA\Calendar\Db\CalendarMapper; +use \OCA\Calendar\Db\ObjectMapper; + +use \OCA\Calendar\Fetcher\Fetcher; +use \OCA\Calendar\Fetcher\CalDAVFetcher; +use \OCA\Calendar\Fetcher\WebCalFetcher; + +use \OCA\Calendar\Utility\Updater; + +class App extends \OCP\AppFramework\App { + + public function __construct($params = array()) { + parent::__construct('calendar', $params); + + /** + * Controller + */ + $this->getContainer()->registerService('CalendarController', function(IAppContainer $c) { + $req = $c->query('Request'); + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new CalendarController($c, $req, $cbl, $obl); + }); + + $this->getContainer()->registerService('ObjectController', function(IAppContainer $c) { + $req = $c->query('Request'); + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new ObjectController($c, $req, $cbl, $obl); + }); + + $this->getContainer()->registerService('EventController', function(IAppContainer $c) { + $req = $c->query('Request'); + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new EventController($c, $req, $cbl, $obl); + }); + + $this->getContainer()->registerService('JournalController', function(IAppContainer $c) { + $req = $c->query('Request'); + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new JournalController($c, $req, $cbl, $obl); + }); + + $this->getContainer()->registerService('TodoController', function(IAppContainer $c) { + $req = $c->query('Request'); + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new TodoController($c, $req, $cbl, $obl); + }); + + $this->getContainer()->registerService('ViewController', function(IAppContainer $c) { + $req = $c->query('Request'); + + return new ViewController($c, $req); + }); + + /** + * BusinessLayer + */ + $this->getContainer()->registerService('CalendarBusinessLayer', function(IAppContainer $c) { + $bbl = $c->query('BackendMapper'); + $cmp = $c->query('CalendarMapper'); + $obl = $c->query('ObjectBusinessLayer'); + + return new CalendarBusinessLayer($c, $bbl, $cmp, $obl); + }); + + $this->getContainer()->registerService('ObjectBusinessLayer', function(IAppContainer $c) { + $bbl = $c->query('BackendMapper'); + $omp = $c->query('ObjectMapper'); + + return new ObjectBusinessLayer($c, $bbl, $omp); + }); + + /** + * Mappers + */ + $this->getContainer()->registerService('BackendMapper', function(IAppContainer $c) { + return new BackendMapper($c); + }); + + $this->getContainer()->registerService('CalendarMapper', function(IAppContainer $c) { + return new CalendarMapper($c); + }); + + $this->getContainer()->registerService('ObjectMapper', function(IAppContainer $c) { + return new ObjectMapper($c); + }); + + /** + * External API + */ + $this->getContainer()->registerService('CalendarAPI', function(IAppContainer $c) { + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new CalendarAPI($c, $cbl, $obl); + }); + + $this->getContainer()->registerService('ObjectAPI', function(IAppContainer $c) { + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new ObjectAPI($c, $cbl, $obl); + }); + + $this->getContainer()->registerService('EventAPI', function(IAppContainer $c) { + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new EventAPI($c, $cbl, $obl); + }); + + $this->getContainer()->registerService('JournalAPI', function(IAppContainer $c) { + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new JournalAPI($c, $cbl, $obl); + }); + + $this->getContainer()->registerService('TodoAPI', function(IAppContainer $c) { + $cbl = $c->query('CalendarBusinessLayer'); + $obl = $c->query('ObjectBusinessLayer'); + + return new TodoAPI($c, $cbl, $obl); + }); + + $defaultBackend = 'local'; + + $defaultConfig = array( + array ( + 'backend' => 'local', + 'classname' => '\\OCA\\Calendar\\Backend\\Local', + 'arguments' => array(), + 'enabled' => true, + ), + array ( + 'backend' => 'birthday', + 'classname' => '\\OCA\\Calendar\\Backend\\Birthday', + 'arguments' => array(), + 'enabled' => true, + ), + array ( + 'backend' => 'anniversary', + 'classname' => '\\OCA\\Calendar\\Backend\\Anniversary', + 'arguments' => array(), + 'enabled' => true, + ), + ); + + $this->getContainer()->registerParameter('defaultBackend', $defaultBackend); + $this->getContainer()->registerParameter('fallbackBackendConfig', $defaultConfig); + } + + public function registerNavigation() { + $appName = $this->getContainer()->getAppName(); + $server = $this->getContainer()->getServer(); + $server->getNavigationManager()->add(array( + 'id' => $appName, + 'order' => 10, + 'href' => \OC::$server->getURLGenerator()->linkToRoute('calendar.view.index'), + 'icon' => \OC::$server->getURLGenerator()->imagePath($appName, 'calendar.svg'), + 'name' => \OC::$server->getL10N($appName)->t('Calendar'), + )); + } + + public function registerCron() { + $c = $this->getContainer(); + //$c->addRegularTask('OCA\Calendar\Backgroundjob\Task', 'run'); + } + public function registerHooks() { + $c = $this->getContainer(); + //$c->connectHook('OC_User', 'post_createUser', '\OC\Calendar\Util\UserHooks', 'create'); + //$c->connectHook('OC_User', 'post_deleteUser', '\OC\Calendar\Util\UserHooks', 'delete'); + //$c->connectHook(); + //$c->connectJook(); + } + + public function registerProviders() { + //\OC_Search::registerProvider('\OCA\Calendar\SearchProvider'); + //\OCP\Share::registerBackend('calendar', '\OCA\Calendar\Share\Calendar'); + //\OCP\Share::registerBackend('event', '\OCA\Calendar\Share\Event'); + } +} \ No newline at end of file diff --git a/sabre/backend.php b/sabre/backend.php new file mode 100644 index 000000000..abb38ccee --- /dev/null +++ b/sabre/backend.php @@ -0,0 +1,403 @@ + + * Copyright (c) 2014 Thomas Tanghus + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Sabre; + +use Sabre_CalDAV_Backend_Abstract; +use Sabre_CalDAV_Property_SupportedCalendarComponentSet; + +class Caldav extends Sabre_CalDAV_Backend_Abstract { + + private $calendarBusinessLayer; + private $objectBusinessLayer; + + /** + * List of CalDAV properties, and how they map to database fieldnames + * + * Add your own properties by simply adding on to this array + * + * @var array + */ + public $propertyMap = array( + '{DAV:}displayname' => 'displayname', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + ); + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri, which the basename of the uri with which the calendar is + * accessed. + * * principalUri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * @param string $principalUri + * @return array + */ + public function getCalendarsForUser($principalUri) { + $raw = OC_Calendar_Calendar::allCalendarsWherePrincipalURIIs($principalUri); + + $calendars = array(); + foreach( $raw as $row ) { + $components = explode(',',$row['components']); + + if($row['userid'] != OCP\USER::getUser()) { + $row['uri'] = $row['uri'] . '_shared_by_' . $row['userid']; + } + $calendar = array( + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => 'principals/'.$row['userid'], + '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}getctag' => $row['ctag']?$row['ctag']:'0', + '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}supported-calendar-component-set' + => new Sabre_CalDAV_Property_SupportedCalendarComponentSet($components), + ); + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + $calendars[] = $calendar; + } + if(\OCP\App::isEnabled('contacts')) { + $ctag = 0; + $app = new \OCA\Contacts\App(); + $addressBooks = $app->getAddressBooksForUser(); + foreach($addressBooks as $addressBook) { + $tmp = $addressBook->lastModified(); + if(!is_null($tmp)) { + $ctag = max($ctag, $tmp); + } + } + $calendars[] = array( + 'id' => 'contact_birthdays', + 'uri' => 'contact_birthdays', + '{DAV:}displayname' => (string)OC_Calendar_App::$l10n->t('Contact birthdays'), + 'principaluri' => 'principals/contact_birthdays', + '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}getctag' => $ctag, + '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}supported-calendar-component-set' + => new Sabre_CalDAV_Property_SupportedCalendarComponentSet(array('VEVENT')), + ); + } + return $calendars; + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this calendar in other methods, such as updateCalendar + * + * @param string $principalUri + * @param string $calendarUri + * @param array $properties + * @return mixed + */ + public function createCalendar($principalUri,$calendarUri, array $properties) { + $fieldNames = array( + 'principaluri', + 'uri', + 'ctag', + ); + $values = array( + ':principaluri' => $principalUri, + ':uri' => $calendarUri, + ':ctag' => 1, + ); + + // Default value + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + $fieldNames[] = 'components'; + if (!isset($properties[$sccs])) { + $values[':components'] = 'VEVENT,VTODO'; + } else { + if (!($properties[$sccs] instanceof Sabre_CalDAV_Property_SupportedCalendarComponentSet)) { + throw new Sabre_DAV_Exception('The ' . $sccs . ' property must be of type: Sabre_CalDAV_Property_SupportedCalendarComponentSet'); + } + $values[':components'] = implode(',',$properties[$sccs]->getValue()); + } + + foreach($this->propertyMap as $xmlName=>$dbName) { + if (isset($properties[$xmlName])) { + + $myValue = $properties[$xmlName]; + $values[':' . $dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + if(!isset($newValues['displayname'])) $newValues['displayname'] = 'unnamed'; + if(!isset($newValues['components'])) $newValues['components'] = 'VEVENT,VTODO'; + if(!isset($newValues['timezone'])) $newValues['timezone'] = null; + if(!isset($newValues['calendarorder'])) $newValues['calendarorder'] = 0; + if(!isset($newValues['calendarcolor'])) $newValues['calendarcolor'] = null; + if(!is_null($newValues['calendarcolor']) && strlen($newValues['calendarcolor']) == 9) { + $newValues['calendarcolor'] = substr($newValues['calendarcolor'], 0, 7); + } + + return OC_Calendar_Calendar::addCalendarFromDAVData($principalUri,$calendarUri,$newValues['displayname'],$newValues['components'],$newValues['timezone'],$newValues['calendarorder'],$newValues['calendarcolor']); + } + + /** + * Updates a calendars properties + * + * The properties array uses the propertyName in clark-notation as key, + * and the array value for the property value. In the case a property + * should be deleted, the property value will be null. + * + * This method must be atomic. If one property cannot be changed, the + * entire operation must fail. + * + * If the operation was successful, true can be returned. + * If the operation failed, false can be returned. + * + * Deletion of a non-existant property is always succesful. + * + * Lastly, it is optional to return detailed information about any + * failures. In this case an array should be returned with the following + * structure: + * + * array( + * 403 => array( + * '{DAV:}displayname' => null, + * ), + * 424 => array( + * '{DAV:}owner' => null, + * ) + * ) + * + * In this example it was forbidden to update {DAV:}displayname. + * (403 Forbidden), which in turn also caused {DAV:}owner to fail + * (424 Failed Dependency) because the request needs to be atomic. + * + * @param string $calendarId + * @param array $properties + * @return bool|array + */ + public function updateCalendar($calendarId, array $properties) { + + $newValues = array(); + $result = array( + 200 => array(), // Ok + 403 => array(), // Forbidden + 424 => array(), // Failed Dependency + ); + + $hasError = false; + + foreach($properties as $propertyName=>$propertyValue) { + + // We don't know about this property. + if (!isset($this->propertyMap[$propertyName])) { + $hasError = true; + $result[403][$propertyName] = null; + unset($properties[$propertyName]); + continue; + } + + $fieldName = $this->propertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + + } + + // If there were any errors we need to fail the request + if ($hasError) { + // Properties has the remaining properties + foreach($properties as $propertyName=>$propertyValue) { + $result[424][$propertyName] = null; + } + + // Removing unused statuscodes for cleanliness + foreach($result as $status=>$properties) { + if (is_array($properties) && count($properties)===0) unset($result[$status]); + } + + return $result; + + } + + // Success + if(!isset($newValues['displayname'])) $newValues['displayname'] = null; + if(!isset($newValues['timezone'])) $newValues['timezone'] = null; + if(!isset($newValues['calendarorder'])) $newValues['calendarorder'] = null; + if(!isset($newValues['calendarcolor'])) $newValues['calendarcolor'] = null; + if(!is_null($newValues['calendarcolor']) && strlen($newValues['calendarcolor']) == 9) { + $newValues['calendarcolor'] = substr($newValues['calendarcolor'], 0, 7); + } + + OC_Calendar_Calendar::editCalendar($calendarId,$newValues['displayname'],null,$newValues['timezone'],$newValues['calendarorder'],$newValues['calendarcolor']); + + return true; + + } + + /** + * Delete a calendar and all it's objects + * + * @param string $calendarId + * @return void + */ + public function deleteCalendar($calendarId) { + if(preg_match( '=iCal/[1-4]?.*Mac OS X/10.[1-6](.[0-9])?=', $_SERVER['HTTP_USER_AGENT'] )) { + throw new Sabre_DAV_Exception_Forbidden("Action is not possible with OSX 10.6.x", 403); + } + + OC_Calendar_Calendar::deleteCalendar($calendarId); + } + + /** + * Returns all calendar objects within a calendar object. + * + * Every item contains an array with the following keys: + * * id - unique identifier which will be used for subsequent updates + * * calendardata - The iCalendar-compatible calnedar data + * * uri - a unique key which will be used to construct the uri. This can be any arbitrary string. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * calendarid - The calendarid as it was passed to this function. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * @param string $calendarId + * @return array + */ + public function getCalendarObjects($calendarId) { + $data = array(); + if($calendarId === 'contact_birthdays') { + $app = new \OCA\Contacts\App(); + $addressBooks = $app->getAddressBooksForUser(); + foreach($addressBooks as $addressBook) { + foreach($addressBook->getChildren() as $contact) { + $vevent = $contact->getBirthdayEvent(); + if(is_null($vevent)) { + continue; + } + $data[] = $this->OCAddETag(array( + 'id' => 0, + 'calendarid' => 'contact_birthdays', + 'uri' => $addressBook->getBackend()->name.'::'.$addressBook->getId().'::'.$contact->getId(), + 'lastmodified' => $contact->lastModified(), + 'summary' => $vevent->SUMMARY, + 'calendardata' => $vevent->serialize() + )); + } + } + } else { + foreach(OC_Calendar_Object::all($calendarId) as $row) { + $data[] = $this->OCAddETag($row); + } + } + return $data; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * @param string $calendarId + * @param string $objectUri + * @return array + */ + public function getCalendarObject($calendarId,$objectUri) { + if($calendarId === 'contact_birthdays') { + $objectUriArray = explode('::', $objectUri); + if(count($objectUriArray) === 3) { + $app = new \OCA\Contacts\App(); + list($backend, $addressBookId, $contactId) = $objectUriArray; + $contact = $app->getContact($backend, $addressBookId, $contactId); + $vevent = $contact->getBirthdayEvent(); + if(is_null($vevent)) { + return false; + } + return $this->OCAddETag(array( + 'id' => 0, + 'calendarid' => 'contact_birthdays', + 'uri' => $contact->getBackend()->name.'::'.$contact->getParent()->getId().'::'.$contact->getId(), + 'lastmodified' => $contact->lastModified(), + 'calendardata' => $vevent->serialize() + )); + } + } + $data = OC_Calendar_Object::findWhereDAVDataIs($calendarId,$objectUri); + if(is_array($data)) { + $data = $this->OCAddETag($data); + $object = OC_VObject::parse($data['calendardata']); + if(!$object) { + return false; + } + $object = OC_Calendar_Object::cleanByAccessClass($data['id'], $object); + $data['calendardata'] = $object->serialize(); + } + return $data; + } + + /** + * Creates a new calendar object. + * + * @param string $calendarId + * @param string $objectUri + * @param string $calendarData + * @return void + */ + public function createCalendarObject($calendarId,$objectUri,$calendarData) { + OC_Calendar_Object::addFromDAVData($calendarId,$objectUri,$calendarData); + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * @param string $calendarId + * @param string $objectUri + * @param string $calendarData + * @return void + */ + public function updateCalendarObject($calendarId,$objectUri,$calendarData) { + OC_Calendar_Object::editFromDAVData($calendarId,$objectUri,$calendarData); + } + + /** + * Deletes an existing calendar object. + * + * @param string $calendarId + * @param string $objectUri + * @return void + */ + public function deleteCalendarObject($calendarId,$objectUri) { + OC_Calendar_Object::deleteFromDAVData($calendarId,$objectUri); + } + + /** + * @brief Creates a etag + * @param array $row Database result + * @returns associative array + * + * Adds a key "etag" to the row + */ + private function OCAddETag($row) { + $row['etag'] = '"'.md5($row['calendarid'].$row['uri'].$row['calendardata'].$row['lastmodified']).'"'; + return $row; + } +} diff --git a/sabre/calendar.php b/sabre/calendar.php new file mode 100644 index 000000000..c3fff0f67 --- /dev/null +++ b/sabre/calendar.php @@ -0,0 +1,123 @@ + + * Copyright (c) 2014 Thomas Tanghus + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Sabre; + +use Sabre_CalDAV_Calendar; +use Sabre_DAV_Exception_NotFound; + +/** + * This class overrides Sabre_CalDAV_Calendar::getACL() to return read/write + * permissions based on user and shared state and it overrides + * Sabre_CalDAV_Calendar::getChild() and Sabre_CalDAV_Calendar::getChildren() + * to instantiate OC_Connector_Sabre_CalDAV_CalendarObjects. +*/ +class CalDAV_Calendar extends Sabre_CalDAV_Calendar { + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() { + + $readprincipal = $this->getOwner(); + $writeprincipal = $this->getOwner(); + $uid = OC_Calendar_Calendar::extractUserID($this->getOwner()); + + if($uid != OCP\USER::getUser()) { + if($uid === 'contact_birthdays') { + $readprincipal = 'principals/' . OCP\User::getUser(); + } else { + $sharedCalendar = OCP\Share::getItemSharedWithBySource('calendar', $this->calendarInfo['id']); + if ($sharedCalendar && ($sharedCalendar['permissions'] & OCP\PERMISSION_READ)) { + $readprincipal = 'principals/' . OCP\User::getUser(); + } + if ($sharedCalendar && ($sharedCalendar['permissions'] & OCP\PERMISSION_UPDATE)) { + $writeprincipal = 'principals/' . OCP\User::getUser(); + } + } + } + + return array( + array( + 'privilege' => '{DAV:}read', + 'principal' => $readprincipal, + 'protected' => true, + ), + array( + 'privilege' => '{DAV:}write', + 'principal' => $writeprincipal, + 'protected' => true, + ), + array( + 'privilege' => '{DAV:}read', + 'principal' => $readprincipal . '/calendar-proxy-write', + 'protected' => true, + ), + array( + 'privilege' => '{DAV:}write', + 'principal' => $writeprincipal . '/calendar-proxy-write', + 'protected' => true, + ), + array( + 'privilege' => '{DAV:}read', + 'principal' => $readprincipal . '/calendar-proxy-read', + 'protected' => true, + ), + array( + 'privilege' => '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ), + + ); + + } + + /** + * Returns a calendar object + * + * The contained calendar objects are for example Events or Todo's. + * + * @param string $name + * @return Sabre_DAV_ICalendarObject + */ + public function getChild($name) { + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name); + if (!$obj) { + throw new Sabre_DAV_Exception_NotFound('Calendar object not found'); + } + return new CalDAV_CalendarObject($this->caldavBackend,$this->calendarInfo,$obj); + + } + + /** + * Returns the full list of calendar objects + * + * @return array + */ + public function getChildren() { + + $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']); + $children = array(); + foreach($objs as $obj) { + $children[] = new CalDAV_CalendarObject($this->caldavBackend,$this->calendarInfo,$obj); + } + return $children; + + } + +} \ No newline at end of file diff --git a/sabre/calendarroot.php b/sabre/calendarroot.php new file mode 100644 index 000000000..9bc7122d9 --- /dev/null +++ b/sabre/calendarroot.php @@ -0,0 +1,36 @@ + + * Copyright (c) 2014 Thomas Tanghus + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Sabre; + +use Sabre_CalDAV_CalendarRootNode; + +/** + * This class overrides Sabre_CalDAV_CalendarRootNode::getChildForPrincipal() + * to instantiate OC_Connector_Sabre_CalDAV_UserCalendars. +*/ +class CalDAV_CalendarRoot extends Sabre_CalDAV_CalendarRootNode { + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @param array $principal + * @return Sabre_DAV_INode + */ + public function getChildForPrincipal(array $principal) { + + return new CalDAV_UserCalendars($this->principalBackend, + $this->caldavBackend, + $principal['uri']); + + } +} \ No newline at end of file diff --git a/sabre/main.php b/sabre/main.php new file mode 100644 index 000000000..e69de29bb diff --git a/sabre/object.php b/sabre/object.php new file mode 100644 index 000000000..773958ea0 --- /dev/null +++ b/sabre/object.php @@ -0,0 +1,83 @@ + + * Copyright (c) 2014 Thomas Tanghus + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Sabre; + +use Sabre_CalDAV_CalendarObject; + +/** + * This class overrides Sabre_CalDAV_CalendarObject::getACL() + * to return read/write permissions based on user and shared state. +*/ +class CalDAV_CalendarObject extends Sabre_CalDAV_CalendarObject { + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() { + + $readprincipal = $this->getOwner(); + $writeprincipal = $this->getOwner(); + $uid = OC_Calendar_Calendar::extractUserID($this->getOwner()); + + if($uid != OCP\USER::getUser()) { + if($uid === 'contact_birthdays') { + $readprincipal = 'principals/' . OCP\User::getUser(); + } else { + $object = OC_VObject::parse($this->objectData['calendardata']); + $sharedCalendar = OCP\Share::getItemSharedWithBySource('calendar', $this->calendarInfo['id']); + $sharedAccessClassPermissions = OC_Calendar_Object::getAccessClassPermissions($object); + if ($sharedCalendar && ($sharedCalendar['permissions'] & OCP\PERMISSION_READ) && ($sharedAccessClassPermissions & OCP\PERMISSION_READ)) { + $readprincipal = 'principals/' . OCP\USER::getUser(); + } + if ($sharedCalendar && ($sharedCalendar['permissions'] & OCP\PERMISSION_UPDATE) && ($sharedAccessClassPermissions & OCP\PERMISSION_UPDATE)) { + $writeprincipal = 'principals/' . OCP\USER::getUser(); + } + } + } + + return array( + array( + 'privilege' => '{DAV:}read', + 'principal' => $readprincipal, + 'protected' => true, + ), + array( + 'privilege' => '{DAV:}write', + 'principal' => $writeprincipal, + 'protected' => true, + ), + array( + 'privilege' => '{DAV:}read', + 'principal' => $readprincipal . '/calendar-proxy-write', + 'protected' => true, + ), + array( + 'privilege' => '{DAV:}write', + 'principal' => $writeprincipal . '/calendar-proxy-write', + 'protected' => true, + ), + array( + 'privilege' => '{DAV:}read', + 'principal' => $readprincipal . '/calendar-proxy-read', + 'protected' => true, + ), + ); + + } + +} \ No newline at end of file diff --git a/sabre/property/datetime.php b/sabre/property/datetime.php new file mode 100644 index 000000000..d25b98721 --- /dev/null +++ b/sabre/property/datetime.php @@ -0,0 +1,20 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\SabreProperty; + +class DateTime extends \OCA\Calendar\Sabre\VObject\Property\DateTime { + + public function getJsonValue() { + $array = parent::getJsonValue(); + + //do smth + + return $array; + } + +} \ No newline at end of file diff --git a/sabre/usercalendars.php b/sabre/usercalendars.php new file mode 100644 index 000000000..a5148727f --- /dev/null +++ b/sabre/usercalendars.php @@ -0,0 +1,39 @@ + + * Copyright (c) 2014 Thomas Tanghus + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Sabre; + +use Sabre_CalDAV_UserCalendars; +use Sabre_CalDAV_Schedule_Outbox; + +/** + * This class overrides Sabre_CalDAV_UserCalendars::getChildren() + * to instantiate OCA\Calendar\Sabre\CalDAV_Calendars. +*/ +class CalDAV_UserCalendars extends Sabre_CalDAV_UserCalendars { + + /** + * Returns a list of calendars + * + * @return array + */ + public function getChildren() { + + $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']); + $objs = array(); + foreach($calendars as $calendar) { + $objs[] = new CalDAV_Calendar($this->principalBackend, + $this->caldavBackend, + $calendar); + } + $objs[] = new Sabre_CalDAV_Schedule_Outbox($this->principalInfo['uri']); + return $objs; + + } + +} \ No newline at end of file diff --git a/sharing/public.php b/sharing/public.php new file mode 100644 index 000000000..e69de29bb diff --git a/templates/app.php b/templates/app.php new file mode 100644 index 000000000..e69de29bb diff --git a/templates/newcalendar.json b/templates/newcalendar.json new file mode 100644 index 000000000..e5bda47e8 --- /dev/null +++ b/templates/newcalendar.json @@ -0,0 +1,13 @@ +{ + "displayname" : "", + "calendarURI" : "", + "color" : "#FFFFFF", + "order" : 0, + "enabled" : false, + "components" : { + "vevent" : false, + "vjournal" : false, + "vtodo" : false + }, + "timezone" : "" +} \ No newline at end of file diff --git a/templates/newobject.json b/templates/newobject.json new file mode 100644 index 000000000..e69de29bb diff --git a/tests/alarm.php b/tests/alarm.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/attendee.php b/tests/attendee.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/backend.php b/tests/backend.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/calendar.php b/tests/calendar.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/event.php b/tests/event.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/journal.php b/tests/journal.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/object.php b/tests/object.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/todo.php b/tests/todo.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/untitled file b/tests/untitled file new file mode 100644 index 000000000..e69de29bb diff --git a/utility/backend.php b/utility/backend.php new file mode 100644 index 000000000..c8ccdc514 --- /dev/null +++ b/utility/backend.php @@ -0,0 +1,12 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Utility; + +class BackendUtility extends Utility{ + +} \ No newline at end of file diff --git a/utility/calendar.php b/utility/calendar.php new file mode 100644 index 000000000..e0f3d011e --- /dev/null +++ b/utility/calendar.php @@ -0,0 +1,49 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Utility; + +class CalendarUtility extends Utility{ + + const SEPERATOR = '-'; + + public static function suggestURI($calendarURI) { + if(substr_count($calendarURI, '-') === 0) { + $calendarURI . '-1'; + } else { + $positionLastDash = strrpos($calendarURI, '-'); + $firstPart = substr($calendarURI, 0, strlen($calendarURI) - $positionLastDash); + $lastPart = substr($calendarURI, $positionLastDash + 1); + $pattern = "^\d$"; + if(preg_match($pattern, $lastPart)) { + $lastPart = (int) $lastPart; + $lastPart++; + $calendarURI = $firstPart . '-' . $lastPart; + } else { + $calendarURI . '-1'; + } + } + } + + public static function splitURI($publicURI) { + if ( $publicURI === false || $publicURI === null || $publicURI === '' ) { + return array(false, false); + } + if ( substr_count($publicURI, self::SEPERATOR) === 0 ){ + return array(false, false); + } + + list($backend, $realCalendarURI) = explode(self::SEPERATOR, $publicURI, 2); + + return array($backend, $realCalendarURI); + } + + public static function getURI($backend, $calendarURI) { + return $backend . self::SEPERATOR . $calendarURI; + } + +} \ No newline at end of file diff --git a/utility/json.php b/utility/json.php new file mode 100644 index 000000000..67150dc27 --- /dev/null +++ b/utility/json.php @@ -0,0 +1,189 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Utility; + +use \OCA\Calendar\Db\ObjectType; +use \OCA\Calendar\Db\Permissions; + +class JSONUtility extends Utility{ + + public static function getUserInformation($userId) { + if($userId === null) { + $userId = \OCP\User::getUser(); + } + + return array( + 'userid' => $userId, + 'displayname' => \OCP\User::getDisplayName($userId), + ); + } + + public static function parseUserInformation($value) { + if(is_array($value) === false) { + return null; + } + + if(array_key_exists('userid', $value) === false) { + return null; + } else { + return $value['userid']; + } + } + + public static function getComponents($components) { + return array( + 'vevent' => (bool) ($components & ObjectType::EVENT), + 'vjournal' => (bool) ($components & ObjectType::JOURNAL), + 'vtodo' => (bool) ($components & ObjectType::TODO), + ); + } + + public static function parseComponents($value) { + if(is_array($value) === false) { + return null; + } + + $components = 0; + + if(array_key_exists('vevent', $value) && $value['vevent'] === true) { + $components += ObjectType::EVENT; + } + if(array_key_exists('vjournal', $value) && $value['vjournal'] === true) { + $components += ObjectType::JOURNAL; + } + if(array_key_exists('vtodo', $value) && $value['vtodo'] === true) { + $components += ObjectType::TODO; + } + + return $components; + } + + public static function getCruds($cruds) { + return array( + 'code' => $cruds, + 'create' => (bool) ($cruds & Permissions::CREATE), + 'read' => (bool) ($cruds & Permissions::READ), + 'update' => (bool) ($cruds & Permissions::UPDATE), + 'delete' => (bool) ($cruds & Permissions::DELETE), + 'share' => (bool) ($cruds & Permissions::SHARE), + ); + } + + public static function parseCruds($value) { + if(is_array($value) === false) { + return null; + } + + $cruds = 0; + + //use code if given + if(array_key_exists('code', $value) && (int) $value['code'] >= 0 && (int) $value['code'] <= 31) { + $cruds = (int) $value['code']; + } else { + if(array_key_exists('create', $value) && $value['create'] === true) { + $cruds += Permissions::CREATE; + } + if(array_key_exists('update', $value) && $value['update'] === true) { + $cruds += Permissions::UPDATE; + } + if(array_key_exists('delete', $value) && $value['delete'] === true) { + $cruds += Permissions::DELETE; + } + if(array_key_exists('read', $value) && $value['read'] === true) { + $cruds += Permissions::READ; + } + if(array_key_exists('share', $value) && $value['share'] === true) { + $cruds += Permissions::SHARE; + } + } + } + + public static function parseCalendarURIForBackend($calendarURI) { + list($backend, $uri) = CalendarUtility::splitURI($calendarURI); + if($backend === false) { + return null; + } + + return $backend; + } + + public static function parseCalendarURIForURI($calendarURI) { + list($backend, $uri) = CalendarUtility::splitURI($calendarURI); + if($uri === false) { + return null; + } + + return $uri; + } + + public static function parseCalendarURI($key, $value) { + list($backend, $calendarURI) = CalendarUtility::splitURI($value); + + if($backend === false || $calendarURI === false) { + return null; + } + + return array( + $backend, + $calendarURI + ); + } + + public static function getTimeZone($timezone, $convenience) { + $jsonTimezone = new JSONTimezone($timezone); + return $jsonTimezone->serialize($convenience); + } + + public static function parseTimeZone($value) { + //$timezoneReader = new JSONTimezoneReader($value); + //return $timezoneReader->getObject(); + return null; + } + + public static function getURL($calendarURI) { + $properties = array( + 'calendarId' => $calendarURI, + ); + + $url = \OCP\Util::linkToRoute('calendar.calendars.show', $properties); + $this->url = \OCP\Util::linkToAbsolute('', substr($url, 1)); + } + + public static function addConvenience(&$vobject) { + $dtstart = SabreUtility::getDTStart($vobject); + if($dtstart !== null) { + $vobject->{'X-OC-DTSTART'} = $dtstart->getDateTime()->format(\DateTime::ISO8601); + } + + $dtend = SabreUtility::getDTEnd($vobject); + if($dtend !== null) { + $vobject->{'X-OC-DTEND'} = $dtend->getDateTime()->format(\DateTime::ISO8601); + } + + //extending SabreDAV's date and datetime jsonSerialize method would probably be easier + //iterate over all VALARMs + //iterate over all VTIMEZONEs + //iterate over all FREEBUSY + //DTSTART + //DTEND + //CREATED + //DTSTAMP + //LAST-MODIFIED + //DUE + //COMPLETED + //RECCURENCE-ID + } + + public static function dropAttachements(&$vobject) { + + } + + public static function removeConvenience(&$vobject) { + + } +} \ No newline at end of file diff --git a/utility/object.php b/utility/object.php new file mode 100644 index 000000000..eab1ae454 --- /dev/null +++ b/utility/object.php @@ -0,0 +1,24 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Utility; + +class ObjectUtility extends Utility{ + + /** + * @brief generate a random uri + * @return string random uri + */ + public static function randomURI() { + $random = rand().time().rand(); + $md5 = md5($random); + $substr = substr($md5, rand(0,11),20); + + return $substr; + } + +} \ No newline at end of file diff --git a/utility/sabre.php b/utility/sabre.php new file mode 100644 index 000000000..7890c6b58 --- /dev/null +++ b/utility/sabre.php @@ -0,0 +1,156 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Utility; + +use \DateTimeZone; + +use \OCA\Calendar\Sabre\VObject\Component; +use \OCA\Calendar\Sabre\VObject\Component\VCalendar; +use \OCA\Calendar\Sabre\VObject\Component\VEvent; +use \OCA\Calendar\Sabre\VObject\Component\VJournal; +use \OCA\Calendar\Sabre\VObject\Component\VTodo; +use \OCA\Calendar\Sabre\VObject\Component\VFreeBusy; + +class SabreUtility extends Utility { + + /** + * @brief get property name of first object + * @param \OCA\Calendar\Sabre\VObject\Component\VCalendar $vcalendar + * @return string property name + */ + public static function getObjectName($vcalendar) { + foreach($vcalendar->children() as $child) { + if($child instanceof VEvent || + $child instanceof VJournal || + $child instanceof VTodo || + $child instanceof VFreeBusy) { + return $child->name; + } + } + } + + /** + * @brief count number of events, journals, todos + * @param \OCA\Calendar\Sabre\VObject\Component\VCalendar $vcalendar + * @return integer + */ + public static function countObjects(VCalendar $vcalendar) { + return self::countSabreObjects($vcalendar, array('VEVENT', 'VJOURNAL', 'VTODO')); + } + + /** + * @brief count number of freebusys + * @param \OCA\Calendar\Sabre\VObject\Component\VCalendar $vcalendar + * @return integer + */ + public static function countFreeBusys(VCalendar $vcalendar) { + return self::countSabreObjects($vcalendar, array('VFREEBUSY')); + } + + /** + * @brief count number of timezones + * @param \OCA\Calendar\Sabre\VObject\Component\VCalendar $vcalendar + * @return integer + */ + public static function countTimezones(VCalendar $vcalendar) { + return self::countSabreObjects($vcalendar, array('VTIMEZONE')); + } + + /** + * @brief count number of components by identifier definied in $properties + * @param \OCA\Calendar\Sabre\VObject\Component\VCalendar $vcalendar + * @param array $properties + * @return integer + */ + public function countSabreObjects(VCalendar $vcalendar, $properties) { + $count = 0; + + foreach($properties as $property) { + if(isset($vcalendar->$property)) { + if(is_array($vcalendar->$object)) { + $count += count($vcalendar->$object); + } + if($vcalendar->$object instanceof Component) { + $count++; + } + } + } + + return $count; + } + + /** + * @brief count number of unique UIDs inside a calendar + * @param \OCA\Calendar\Sabre\VObject\Component\VCalendar $vcalendar + * @return integer + */ + public static function countUniqueUIDs(VCalendar $vcalendar) { + $uids = array(); + + foreach($vcalendar->children() as $child) { + if($child instanceof VEvent || + $child instanceof VJournal || + $child instanceof VTodo || + $child instanceof VFreeBusy) { + $uids[] = (string) $child->{'UID'}; + } + } + + $uniqueUIDs = array_unique($uids); + $numberOfUniqueUIDs = count($uniqueUIDs); + + return $numberOfUniqueUIDs; + } + + /** + * @brief get dtstart property of object + * @param \OCA\Calendar\Sabre\VObject\Component $vobject + * @return \OCA\Calendar\Sabre\VObject\Property\DateTime $dstart + */ + public static function getDTStart(Component $vobject) { + if(!isset($vobject->{'DTSTART'})) { + return null; + } + + //If recurrenceId is set, that's the actual start + //DTSTART has the value of the first object of recurring events + //This doesn't make any sense, but that's how it is in the standard + if(isset($vobject->{'RECURRENCE-ID'})) { + return $vobject->{'RECURRENCE-ID'}; + } + + return $vobject->{'DTSTART'}; + } + + /** + * @brief get dtend property of object + * @param \OCA\Calendar\Sabre\VObject\Component $vobject + * @return \OCA\Calendar\Sabre\VObject\Property\DateTime $dtend + */ + public static function getDTEnd(Component $vobject) { + if(isset($vobject->{'DTEND'})) { + return $vobject->{'DTEND'}; + } + + if(!isset($vobject->{'DTSTART'})) { + return null; + } + + $dtend = self::getDTStart($vobject); + + if(!isset($vobject->{'DURATION'})) { + return $dtend; + } + + $interval = $vobject->{'DURATION'}->getDateInterval(); + + $dtend->getDateTime()->add($interval); + + return $dtend; + } +} \ No newline at end of file diff --git a/utility/update.php b/utility/update.php new file mode 100644 index 000000000..a631e1d66 --- /dev/null +++ b/utility/update.php @@ -0,0 +1,12 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Utility; + +class UpdateUtility extends Utility{ + +} \ No newline at end of file diff --git a/utility/utility.php b/utility/utility.php new file mode 100644 index 000000000..60fb939fa --- /dev/null +++ b/utility/utility.php @@ -0,0 +1,38 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OCA\Calendar\Utility; + +use \DateTimeZone; +use \OCA\Calendar\Sabre\VObject\Component; + +class Utility { + + public static function slugify($string) { + $string = preg_replace('~[^\\pL\d\.]+~u', '-', $string); + $string = trim($string, '-'); + + if (function_exists('iconv')) { + $string = iconv('utf-8', 'us-ascii//TRANSLIT//IGNORE', $string); + } + + $string = strtolower($string); + $string = preg_replace('~[^-\w\.]+~', '', $string); + $string = preg_replace('~\.+$~', '', $string); + + if (empty($string)) { + return uniqid(); + } + + return $string; + } + + public static function isTimezoneSupported($timezone) { + $supportedTimezones = DateTimeZone::listIdentifiers(); + return in_array($timezone, $supportedTimezones); + } +} \ No newline at end of file