From 560839195e97abdaca4f8c144a6a760a0ab90c19 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Sun, 7 Jul 2013 20:06:14 +0200 Subject: make l10n libs capable of handling plural translations --- lib/l10n.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'lib/l10n.php') diff --git a/lib/l10n.php b/lib/l10n.php index d35ce5fed14..03528c22746 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -166,6 +166,26 @@ class OC_L10N{ return new OC_L10N_String($this, $text, $parameters); } + /** + * @brief Translating + * @param $text_singular String the string to translate for exactly one object + * @param $text_plural String the string to translate for n objects + * @param $count Integer Number of objects + * @param array $parameters default:array() Parameters for sprintf + * @return \OC_L10N_String Translation or the same text + * + * Returns the translation. If no translation is found, $text will be + * returned. %n will be replaced with the number of objects. + */ + public function tp($text_singular, $text_plural, $count, $parameters = array()) { + if($count == 1){ + return new OC_L10N_String($this, $text_singular, $parameters, $count); + } + else{ + return new OC_L10N_String($this, $text_plural, $parameters, $count); + } + } + /** * @brief Translating * @param $textArray The text array we need a translation for -- cgit v1.2.3 From bb0c5bff5fc493dd24c2c9cbbf27986fd5098de1 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Tue, 16 Jul 2013 22:16:53 +0200 Subject: Add multiple plural forms to the php part --- core/js/js.js | 11 +++++------ lib/l10n.php | 34 ++++++++++++++++++++++++++++++---- lib/l10n/string.php | 9 ++++++++- 3 files changed, 43 insertions(+), 11 deletions(-) (limited to 'lib/l10n.php') diff --git a/core/js/js.js b/core/js/js.js index 15cf1d286fa..37540bd15e7 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -33,7 +33,7 @@ if (oc_debug !== true || typeof console === "undefined" || typeof console.log == * @return string */ function t(app, text, vars, count){ - if( !( t.cache[app] )){ + if( !( t.cache[app] )) { $.ajax(OC.filePath('core','ajax','translations.php'),{ async:false,//todo a proper sollution for this without sync ajax calls data:{'app': app}, @@ -49,8 +49,7 @@ function t(app, text, vars, count){ } } var _build = function (text, vars, count) { - // FIXME: replace %n with content of count - return text.replace(/{([^{}]*)}/g, + return text.replace(/%n/g, count).replace(/{([^{}]*)}/g, function (a, b) { var r = vars[b]; return typeof r === 'string' || typeof r === 'number' ? r : a; @@ -62,7 +61,7 @@ function t(app, text, vars, count){ translation = t.cache[app][text]; } - if(typeof vars === 'object' || typeof count !== 'undefined' ) { + if(typeof vars === 'object' || count !== undefined ) { return _build(translation, vars, count); } else { return translation; @@ -79,8 +78,8 @@ t.cache={}; * @param vars (optional) FIXME * @return string */ -function tp(app, text_singular, text_plural, count, vars){ - if(count==1){ +function n(app, text_singular, text_plural, count, vars){ + if(count === 1) { return t(app, text_singular, vars, count); } else{ diff --git a/lib/l10n.php b/lib/l10n.php index 03528c22746..8348962cc10 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -176,13 +176,28 @@ class OC_L10N{ * * Returns the translation. If no translation is found, $text will be * returned. %n will be replaced with the number of objects. + * + * In case there is more than one plural form you can add a function + * "selectplural" in core/l10n/l10n-*.php + * + * Example: + * + * [...] + * 'selectplural' => function($i){return $i == 1 ? 0 : $i == 2 ? 1 : 2}, + * [...] */ - public function tp($text_singular, $text_plural, $count, $parameters = array()) { - if($count == 1){ - return new OC_L10N_String($this, $text_singular, $parameters, $count); + public function n($text_singular, $text_plural, $count, $parameters = array()) { + $identifier = "_${text_singular}__${text_plural}_"; + if(array_key_exists( $this->localizations, "selectplural") && array_key_exists($this->translations, $identifier)) { + return new OC_L10N_String( $this, $identifier, $parameters, $count ); } else{ - return new OC_L10N_String($this, $text_plural, $parameters, $count); + if($count === 1) { + return new OC_L10N_String($this, $text_singular, $parameters, $count); + } + else{ + return new OC_L10N_String($this, $text_plural, $parameters, $count); + } } } @@ -220,6 +235,17 @@ class OC_L10N{ return $this->translations; } + /** + * @brief get localizations + * @returns Fetch all localizations + * + * Returns an associative array with all localizations + */ + public function getLocalizations() { + $this->init(); + return $this->localizations; + } + /** * @brief Localization * @param $type Type of localization diff --git a/lib/l10n/string.php b/lib/l10n/string.php index 1bef7330945..3cda5eab506 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -18,10 +18,17 @@ class OC_L10N_String{ public function __toString() { $translations = $this->l10n->getTranslations(); + $localizations = $this->l10n->getLocalizations(); $text = $this->text; if(array_key_exists($this->text, $translations)) { - $text = $translations[$this->text]; + if(is_array($translations[$this->text])) { + $id = $localizations["selectplural"]( $count ); + $text = $translations[$this->text][$id] + } + else{ + $text = $translations[$this->text]; + } } // Replace %n first (won't interfere with vsprintf) $text = str_replace('%n', $this->count, $text); -- cgit v1.2.3 From 495e44e76c15d43eb847dd73317bdb9d6926ff31 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Thu, 1 Aug 2013 19:54:51 +0200 Subject: Update l10n.php to read plural_forms string --- lib/l10n.php | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) (limited to 'lib/l10n.php') diff --git a/lib/l10n.php b/lib/l10n.php index 8348962cc10..d24717a23a2 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -54,6 +54,11 @@ class OC_L10N{ */ private $translations = array(); + /** + * Plural forms + */ + private $plural_forms = ""; + /** * Localization */ @@ -138,6 +143,9 @@ class OC_L10N{ } } } + if(isset($PLURAL_FORMS)) { + $this->plural_forms = $PLURAL_FORMS; + } } if(file_exists(OC::$SERVERROOT.'/core/l10n/l10n-'.$lang.'.php')) { @@ -177,18 +185,13 @@ class OC_L10N{ * Returns the translation. If no translation is found, $text will be * returned. %n will be replaced with the number of objects. * - * In case there is more than one plural form you can add a function - * "selectplural" in core/l10n/l10n-*.php - * - * Example: + * The correct plural is determined by the plural_forms-function + * provided by the po file. * - * [...] - * 'selectplural' => function($i){return $i == 1 ? 0 : $i == 2 ? 1 : 2}, - * [...] */ public function n($text_singular, $text_plural, $count, $parameters = array()) { $identifier = "_${text_singular}__${text_plural}_"; - if(array_key_exists( $this->localizations, "selectplural") && array_key_exists($this->translations, $identifier)) { + if( array_key_exists($this->translations, $identifier)) { return new OC_L10N_String( $this, $identifier, $parameters, $count ); } else{ @@ -235,6 +238,17 @@ class OC_L10N{ return $this->translations; } + /** + * @brief getPluralForms + * @returns Fetches the gettext "Plural-Forms"-string + * + * Returns a string like "nplurals=2; plural=(n != 1);" + */ + public function getPluralForms() { + $this->init(); + return $this->plural_forms; + } + /** * @brief get localizations * @returns Fetch all localizations -- cgit v1.2.3 From 9673a563b567ee6f580c157a901f038f38f0b5a2 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Thu, 1 Aug 2013 21:18:18 +0200 Subject: Create plural functions on the fly --- lib/l10n.php | 91 +++++++++++++++++++++++++++++++++++++++++++++++++---- lib/l10n/string.php | 2 +- 2 files changed, 86 insertions(+), 7 deletions(-) (limited to 'lib/l10n.php') diff --git a/lib/l10n.php b/lib/l10n.php index d24717a23a2..208fa930c99 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -55,9 +55,14 @@ class OC_L10N{ private $translations = array(); /** - * Plural forms + * Plural forms (string) */ - private $plural_forms = ""; + private $plural_form_string; + + /** + * Plural forms (function) + */ + private $plural_form_function = null; /** * Localization @@ -144,7 +149,7 @@ class OC_L10N{ } } if(isset($PLURAL_FORMS)) { - $this->plural_forms = $PLURAL_FORMS; + $this->plural_form_string = $PLURAL_FORMS; } } @@ -161,6 +166,66 @@ class OC_L10N{ } } + /** + * @brief Creates a function that The constructor + * @param $app the app requesting l10n + * @param $lang default: null Language + * @returns OC_L10N-Object + * + * If language is not set, the constructor tries to find the right + * language. + * + * Parts of the code is copied from Habari: + * https://github.com/habari/system/blob/master/classes/locale.php + */ + protected function createPluralFormFunction($string){ + if(preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) { + // sanitize + $nplurals = preg_replace( '/[^0-9]/', '', $matches[1] ); + $plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] ); + + $body = str_replace( + array( 'plural', 'n', '$n$plurals', ), + array( '$plural', '$n', '$nplurals', ), + 'nplurals='. $nplurals . '; plural=' . $plural + ); + + // add parens + // important since PHP's ternary evaluates from left to right + $body .= ';'; + $res = ''; + $p = 0; + for($i = 0; $i < strlen($body); $i++) { + $ch = $body[$i]; + switch ( $ch ) { + case '?': + $res .= ' ? ('; + $p++; + break; + case ':': + $res .= ') : ('; + break; + case ';': + $res .= str_repeat( ')', $p ) . ';'; + $p = 0; + break; + default: + $res .= $ch; + } + } + + $body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);'; + return create_function('$n', $body); + } + else { + // default: one plural form for all cases but n==1 (english) + return create_function( + '$n', + '$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);' + ); + } + } + /** * @brief Translating * @param $text String The text we need a translation for @@ -239,14 +304,28 @@ class OC_L10N{ } /** - * @brief getPluralForms + * @brief getPluralFormString * @returns Fetches the gettext "Plural-Forms"-string * * Returns a string like "nplurals=2; plural=(n != 1);" */ - public function getPluralForms() { + public function getPluralFormString() { $this->init(); - return $this->plural_forms; + return $this->plural_form_string; + } + + /** + * @brief getPluralFormFunction + * @returns returns the plural form function + * + * returned function accepts the argument $n + */ + public function getPluralFormString() { + $this->init(); + if(is_null($this->plural_form_function)) { + $this->plural_form_function = createPluralFormFunction($this->plural_form_string); + } + return $this->plural_form_function; } /** diff --git a/lib/l10n/string.php b/lib/l10n/string.php index 5b9dbaee8a2..8d83f1f61fb 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -23,7 +23,7 @@ class OC_L10N_String{ $text = $this->text; if(array_key_exists($this->text, $translations)) { if(is_array($translations[$this->text])) { - $id = $localizations["selectplural"]( $count ); + $id = $l10n->getPluralFormFunction()( $count ); $text = $translations[$this->text][$id]; } else{ -- cgit v1.2.3 From eee2c369d9b743634a062ad0c0b9f85496cecc87 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Thu, 1 Aug 2013 21:36:33 +0200 Subject: Fix copy and paste errors in l10n.php --- lib/l10n.php | 2 +- lib/l10n/string.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/l10n.php') diff --git a/lib/l10n.php b/lib/l10n.php index 208fa930c99..ab2de5d9f19 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -320,7 +320,7 @@ class OC_L10N{ * * returned function accepts the argument $n */ - public function getPluralFormString() { + public function getPluralFormFunction() { $this->init(); if(is_null($this->plural_form_function)) { $this->plural_form_function = createPluralFormFunction($this->plural_form_string); diff --git a/lib/l10n/string.php b/lib/l10n/string.php index 8d83f1f61fb..c72b744a7ec 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -23,7 +23,7 @@ class OC_L10N_String{ $text = $this->text; if(array_key_exists($this->text, $translations)) { if(is_array($translations[$this->text])) { - $id = $l10n->getPluralFormFunction()( $count ); + $id = $l10n->getPluralFormFunction()($count); $text = $translations[$this->text][$id]; } else{ -- cgit v1.2.3 From 35a42da3caaf9f78a625b19b421ddeacb59e3402 Mon Sep 17 00:00:00 2001 From: Thomas Müller Date: Fri, 2 Aug 2013 21:46:25 +0200 Subject: PHPDoc comments adjusted *partially* - fixing call to createPluralFormFunction --- lib/l10n.php | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) (limited to 'lib/l10n.php') diff --git a/lib/l10n.php b/lib/l10n.php index ab2de5d9f19..0e47215369c 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -2,8 +2,10 @@ /** * ownCloud * + * @author Frank Karlitschek * @author Jakob Sack * @copyright 2012 Frank Karlitschek frank@owncloud.org + * @copyright 2013 Jakob Sack * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -76,6 +78,8 @@ class OC_L10N{ /** * get an L10N instance + * @param $app string + * @param $lang string|null * @return OC_L10N */ public static function get($app, $lang=null) { @@ -91,8 +95,8 @@ class OC_L10N{ /** * @brief The constructor - * @param $app the app requesting l10n - * @param $lang default: null Language + * @param $app string app requesting l10n + * @param $lang string default: null Language * @returns OC_L10N-Object * * If language is not set, the constructor tries to find the right @@ -168,15 +172,14 @@ class OC_L10N{ /** * @brief Creates a function that The constructor - * @param $app the app requesting l10n - * @param $lang default: null Language - * @returns OC_L10N-Object * * If language is not set, the constructor tries to find the right * language. * * Parts of the code is copied from Habari: * https://github.com/habari/system/blob/master/classes/locale.php + * @param $string string + * @return string */ protected function createPluralFormFunction($string){ if(preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) { @@ -190,7 +193,7 @@ class OC_L10N{ 'nplurals='. $nplurals . '; plural=' . $plural ); - // add parens + // add parents // important since PHP's ternary evaluates from left to right $body .= ';'; $res = ''; @@ -305,7 +308,7 @@ class OC_L10N{ /** * @brief getPluralFormString - * @returns Fetches the gettext "Plural-Forms"-string + * @returns string containing the gettext "Plural-Forms"-string * * Returns a string like "nplurals=2; plural=(n != 1);" */ @@ -316,14 +319,14 @@ class OC_L10N{ /** * @brief getPluralFormFunction - * @returns returns the plural form function + * @returns string the plural form function * * returned function accepts the argument $n */ public function getPluralFormFunction() { $this->init(); if(is_null($this->plural_form_function)) { - $this->plural_form_function = createPluralFormFunction($this->plural_form_string); + $this->plural_form_function = $this->createPluralFormFunction($this->plural_form_string); } return $this->plural_form_function; } @@ -369,8 +372,12 @@ class OC_L10N{ case 'date': case 'datetime': case 'time': - if($data instanceof DateTime) return $data->format($this->localizations[$type]); - elseif(is_string($data)) $data = strtotime($data); + if($data instanceof DateTime) { + return $data->format($this->localizations[$type]); + } + elseif(is_string($data)) { + $data = strtotime($data); + } $locales = array(self::findLanguage()); if (strlen($locales[0]) == 2) { $locales[] = $locales[0].'_'.strtoupper($locales[0]); -- cgit v1.2.3 From e04bf0aaeb92103ef432e56f1d6d712599952cee Mon Sep 17 00:00:00 2001 From: Thomas Müller Date: Fri, 2 Aug 2013 23:08:41 +0200 Subject: unit tests for plural translations added --- lib/l10n.php | 16 +++++++++++++-- lib/l10n/string.php | 26 ++++++++++++++++++++---- tests/data/l10n/cs.php | 5 +++++ tests/data/l10n/de.php | 5 +++++ tests/data/l10n/ru.php | 5 +++++ tests/lib/l10n.php | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 tests/data/l10n/cs.php create mode 100644 tests/data/l10n/de.php create mode 100644 tests/data/l10n/ru.php create mode 100644 tests/lib/l10n.php (limited to 'lib/l10n.php') diff --git a/lib/l10n.php b/lib/l10n.php index 0e47215369c..a11ed785c5e 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -25,7 +25,7 @@ /** * This class is for i18n and l10n */ -class OC_L10N{ +class OC_L10N { /** * cached instances */ @@ -107,6 +107,17 @@ class OC_L10N{ $this->lang = $lang; } + public function load($transFile) { + $this->app = true; + include $transFile; + if(isset($TRANSLATIONS) && is_array($TRANSLATIONS)) { + $this->translations = $TRANSLATIONS; + } + if(isset($PLURAL_FORMS)) { + $this->plural_form_string = $PLURAL_FORMS; + } + } + protected function init() { if ($this->app === true) { return; @@ -258,8 +269,9 @@ class OC_L10N{ * */ public function n($text_singular, $text_plural, $count, $parameters = array()) { + $this->init(); $identifier = "_${text_singular}__${text_plural}_"; - if( array_key_exists($this->translations, $identifier)) { + if( array_key_exists($identifier, $this->translations)) { return new OC_L10N_String( $this, $identifier, $parameters, $count ); } else{ diff --git a/lib/l10n/string.php b/lib/l10n/string.php index c78b06428d3..88c85b32e70 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -7,30 +7,48 @@ */ class OC_L10N_String{ + /** + * @var OC_L10N + */ protected $l10n; + + /** + * @var string + */ + protected $text; + + /** + * @var array + */ + protected $parameters; + + /** + * @var integer + */ + protected $count; + public function __construct($l10n, $text, $parameters, $count = 1) { $this->l10n = $l10n; $this->text = $text; $this->parameters = $parameters; $this->count = $count; - } public function __toString() { $translations = $this->l10n->getTranslations(); - $localizations = $this->l10n->getLocalizations(); $text = $this->text; if(array_key_exists($this->text, $translations)) { if(is_array($translations[$this->text])) { - $fn = $l10n->getPluralFormFunction(); - $id = $fn($count); + $fn = $this->l10n->getPluralFormFunction(); + $id = $fn($this->count); $text = $translations[$this->text][$id]; } else{ $text = $translations[$this->text]; } } + // Replace %n first (won't interfere with vsprintf) $text = str_replace('%n', $this->count, $text); return vsprintf($text, $this->parameters); diff --git a/tests/data/l10n/cs.php b/tests/data/l10n/cs.php new file mode 100644 index 00000000000..1c5907bc148 --- /dev/null +++ b/tests/data/l10n/cs.php @@ -0,0 +1,5 @@ + array("%n okno", "%n okna", "%n oken") +); +$PLURAL_FORMS = "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"; diff --git a/tests/data/l10n/de.php b/tests/data/l10n/de.php new file mode 100644 index 00000000000..858ec8af49c --- /dev/null +++ b/tests/data/l10n/de.php @@ -0,0 +1,5 @@ + array("%n Datei", "%n Dateien") +); +$PLURAL_FORMS = "nplurals=2; plural=(n != 1);"; diff --git a/tests/data/l10n/ru.php b/tests/data/l10n/ru.php new file mode 100644 index 00000000000..dd46293db6c --- /dev/null +++ b/tests/data/l10n/ru.php @@ -0,0 +1,5 @@ + array("%n файл", "%n файла", "%n файлов") +); +$PLURAL_FORMS = "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"; diff --git a/tests/lib/l10n.php b/tests/lib/l10n.php new file mode 100644 index 00000000000..846a2f22c8e --- /dev/null +++ b/tests/lib/l10n.php @@ -0,0 +1,55 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +class Test_L10n extends PHPUnit_Framework_TestCase { + + public function testGermanPluralTranslations() { + $l = new OC_L10N('test'); + $transFile = OC::$SERVERROOT.'/tests/data/l10n/de.php'; + + $l->load($transFile); + $this->assertEquals('1 Datei', (string)$l->n('%n file', '%n files', 1, array(1))); + $this->assertEquals('2 Dateien', (string)$l->n('%n file', '%n files', 2, array(2))); + } + + public function testRussianPluralTranslations() { + $l = new OC_L10N('test'); + $transFile = OC::$SERVERROOT.'/tests/data/l10n/ru.php'; + + $l->load($transFile); + $this->assertEquals('1 файл', (string)$l->n('%n file', '%n files', 1)); + $this->assertEquals('2 файла', (string)$l->n('%n file', '%n files', 2)); + $this->assertEquals('6 файлов', (string)$l->n('%n file', '%n files', 6)); + $this->assertEquals('21 файл', (string)$l->n('%n file', '%n files', 21)); + $this->assertEquals('22 файла', (string)$l->n('%n file', '%n files', 22)); + $this->assertEquals('26 файлов', (string)$l->n('%n file', '%n files', 26)); + + /* + 1 file 1 файл 1 папка + 2-4 files 2-4 файла 2-4 папки + 5-20 files 5-20 файлов 5-20 папок + 21 files 21 файл 21 папка + 22-24 files 22-24 файла 22-24 папки + 25-30 files 25-30 файлов 25-30 папок + etc + 100 files 100 файлов, 100 папок + 1000 files 1000 файлов 1000 папок + */ + } + + public function testCzechPluralTranslations() { + $l = new OC_L10N('test'); + $transFile = OC::$SERVERROOT.'/tests/data/l10n/cs.php'; + + $l->load($transFile); + $this->assertEquals('1 okno', (string)$l->n('%n window', '%n windows', 1)); + $this->assertEquals('2 okna', (string)$l->n('%n window', '%n windows', 2)); + $this->assertEquals('5 oken', (string)$l->n('%n window', '%n windows', 5)); + } + +} -- cgit v1.2.3 From b229381dda7203402e00cb6f58c3b8a147556b0d Mon Sep 17 00:00:00 2001 From: Thomas Müller Date: Sat, 3 Aug 2013 23:29:31 +0200 Subject: initialize $plural_form_string with English default --- lib/l10n.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/l10n.php') diff --git a/lib/l10n.php b/lib/l10n.php index a11ed785c5e..b1f2fceeb16 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -59,7 +59,7 @@ class OC_L10N { /** * Plural forms (string) */ - private $plural_form_string; + private $plural_form_string = 'nplurals=2; plural=(n != 1);'; /** * Plural forms (function) -- cgit v1.2.3