Completed
Pull Request — master (#7351)
by Björn
25:21 queued 11:16
created
lib/private/L10N/Factory.php 1 patch
Indentation   +377 added lines, -377 removed lines patch added patch discarded remove patch
@@ -40,381 +40,381 @@
 block discarded – undo
40 40
  */
41 41
 class Factory implements IFactory {
42 42
 
43
-	/** @var string */
44
-	protected $requestLanguage = '';
45
-
46
-	/**
47
-	 * cached instances
48
-	 * @var array Structure: Lang => App => \OCP\IL10N
49
-	 */
50
-	protected $instances = [];
51
-
52
-	/**
53
-	 * @var array Structure: App => string[]
54
-	 */
55
-	protected $availableLanguages = [];
56
-
57
-	/**
58
-	 * @var array Structure: string => callable
59
-	 */
60
-	protected $pluralFunctions = [];
61
-
62
-	/** @var IConfig */
63
-	protected $config;
64
-
65
-	/** @var IRequest */
66
-	protected $request;
67
-
68
-	/** @var IUserSession */
69
-	protected $userSession;
70
-
71
-	/** @var string */
72
-	protected $serverRoot;
73
-
74
-	/**
75
-	 * @param IConfig $config
76
-	 * @param IRequest $request
77
-	 * @param IUserSession $userSession
78
-	 * @param string $serverRoot
79
-	 */
80
-	public function __construct(IConfig $config,
81
-								IRequest $request,
82
-								IUserSession $userSession,
83
-								$serverRoot) {
84
-		$this->config = $config;
85
-		$this->request = $request;
86
-		$this->userSession = $userSession;
87
-		$this->serverRoot = $serverRoot;
88
-	}
89
-
90
-	/**
91
-	 * Get a language instance
92
-	 *
93
-	 * @param string $app
94
-	 * @param string|null $lang
95
-	 * @return \OCP\IL10N
96
-	 */
97
-	public function get($app, $lang = null) {
98
-		$app = \OC_App::cleanAppId($app);
99
-		if ($lang !== null) {
100
-			$lang = str_replace(array('\0', '/', '\\', '..'), '', (string) $lang);
101
-		}
102
-
103
-		$forceLang = $this->config->getSystemValue('force_language', false);
104
-		if (is_string($forceLang)) {
105
-			$lang = $forceLang;
106
-		}
107
-
108
-		if ($lang === null || !$this->languageExists($app, $lang)) {
109
-			$lang = $this->findLanguage($app);
110
-		}
111
-
112
-		if (!isset($this->instances[$lang][$app])) {
113
-			$this->instances[$lang][$app] = new L10N(
114
-				$this, $app, $lang,
115
-				$this->getL10nFilesForApp($app, $lang)
116
-			);
117
-		}
118
-
119
-		return $this->instances[$lang][$app];
120
-	}
121
-
122
-	/**
123
-	 * Find the best language
124
-	 *
125
-	 * @param string|null $app App id or null for core
126
-	 * @return string language If nothing works it returns 'en'
127
-	 */
128
-	public function findLanguage($app = null) {
129
-		if ($this->requestLanguage !== '' && $this->languageExists($app, $this->requestLanguage)) {
130
-			return $this->requestLanguage;
131
-		}
132
-
133
-		/**
134
-		 * At this point Nextcloud might not yet be installed and thus the lookup
135
-		 * in the preferences table might fail. For this reason we need to check
136
-		 * whether the instance has already been installed
137
-		 *
138
-		 * @link https://github.com/owncloud/core/issues/21955
139
-		 */
140
-		if($this->config->getSystemValue('installed', false)) {
141
-			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
142
-			if(!is_null($userId)) {
143
-				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
144
-			} else {
145
-				$userLang = null;
146
-			}
147
-		} else {
148
-			$userId = null;
149
-			$userLang = null;
150
-		}
151
-
152
-		if ($userLang) {
153
-			$this->requestLanguage = $userLang;
154
-			if ($this->languageExists($app, $userLang)) {
155
-				return $userLang;
156
-			}
157
-		}
158
-
159
-		try {
160
-			// Try to get the language from the Request
161
-			$lang = $this->getLanguageFromRequest($app);
162
-			// use formal version of german ("Sie" instead of "Du") by default
163
-			$lang = strtolower($lang) === 'de' ? 'de_DE' : $lang;
164
-			if ($userId !== null && $app === null && !$userLang) {
165
-				$this->config->setUserValue($userId, 'core', 'lang', $lang);
166
-			}
167
-			return $lang;
168
-		} catch (LanguageNotFoundException $e) {
169
-			// Finding language from request failed fall back to default language
170
-			$defaultLanguage = $this->config->getSystemValue('default_language', false);
171
-			if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
172
-				return $defaultLanguage;
173
-			}
174
-		}
175
-
176
-		// We could not find any language so fall back to english
177
-		return 'en';
178
-	}
179
-
180
-	/**
181
-	 * Find all available languages for an app
182
-	 *
183
-	 * @param string|null $app App id or null for core
184
-	 * @return array an array of available languages
185
-	 */
186
-	public function findAvailableLanguages($app = null) {
187
-		$key = $app;
188
-		if ($key === null) {
189
-			$key = 'null';
190
-		}
191
-
192
-		// also works with null as key
193
-		if (!empty($this->availableLanguages[$key])) {
194
-			return $this->availableLanguages[$key];
195
-		}
196
-
197
-		$available = ['en']; //english is always available
198
-		$dir = $this->findL10nDir($app);
199
-		if (is_dir($dir)) {
200
-			$files = scandir($dir);
201
-			if ($files !== false) {
202
-				foreach ($files as $file) {
203
-					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
204
-						$available[] = substr($file, 0, -5);
205
-					}
206
-				}
207
-			}
208
-		}
209
-
210
-		// merge with translations from theme
211
-		$theme = $this->config->getSystemValue('theme');
212
-		if (!empty($theme)) {
213
-			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
214
-
215
-			if (is_dir($themeDir)) {
216
-				$files = scandir($themeDir);
217
-				if ($files !== false) {
218
-					foreach ($files as $file) {
219
-						if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
220
-							$available[] = substr($file, 0, -5);
221
-						}
222
-					}
223
-				}
224
-			}
225
-		}
226
-
227
-		$this->availableLanguages[$key] = $available;
228
-		return $available;
229
-	}
230
-
231
-	/**
232
-	 * @param string|null $app App id or null for core
233
-	 * @param string $lang
234
-	 * @return bool
235
-	 */
236
-	public function languageExists($app, $lang) {
237
-		if ($lang === 'en') {//english is always available
238
-			return true;
239
-		}
240
-
241
-		$languages = $this->findAvailableLanguages($app);
242
-		return array_search($lang, $languages) !== false;
243
-	}
244
-
245
-	/**
246
-	 * @param string|null $app
247
-	 * @return string
248
-	 * @throws LanguageNotFoundException
249
-	 */
250
-	private function getLanguageFromRequest($app) {
251
-		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
252
-		if ($header) {
253
-			$available = $this->findAvailableLanguages($app);
254
-
255
-			// E.g. make sure that 'de' is before 'de_DE'.
256
-			sort($available);
257
-
258
-			$preferences = preg_split('/,\s*/', strtolower($header));
259
-			foreach ($preferences as $preference) {
260
-				list($preferred_language) = explode(';', $preference);
261
-				$preferred_language = str_replace('-', '_', $preferred_language);
262
-
263
-				foreach ($available as $available_language) {
264
-					if ($preferred_language === strtolower($available_language)) {
265
-						return $available_language;
266
-					}
267
-				}
268
-
269
-				// Fallback from de_De to de
270
-				foreach ($available as $available_language) {
271
-					if (substr($preferred_language, 0, 2) === $available_language) {
272
-						return $available_language;
273
-					}
274
-				}
275
-			}
276
-		}
277
-
278
-		throw new LanguageNotFoundException();
279
-	}
280
-
281
-	/**
282
-	 * Checks if $sub is a subdirectory of $parent
283
-	 *
284
-	 * @param string $sub
285
-	 * @param string $parent
286
-	 * @return bool
287
-	 */
288
-	private function isSubDirectory($sub, $parent) {
289
-		// Check whether $sub contains no ".."
290
-		if(strpos($sub, '..') !== false) {
291
-			return false;
292
-		}
293
-
294
-		// Check whether $sub is a subdirectory of $parent
295
-		if (strpos($sub, $parent) === 0) {
296
-			return true;
297
-		}
298
-
299
-		return false;
300
-	}
301
-
302
-	/**
303
-	 * Get a list of language files that should be loaded
304
-	 *
305
-	 * @param string $app
306
-	 * @param string $lang
307
-	 * @return string[]
308
-	 */
309
-	// FIXME This method is only public, until OC_L10N does not need it anymore,
310
-	// FIXME This is also the reason, why it is not in the public interface
311
-	public function getL10nFilesForApp($app, $lang) {
312
-		$languageFiles = [];
313
-
314
-		$i18nDir = $this->findL10nDir($app);
315
-		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
316
-
317
-		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
318
-				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
319
-				|| $this->isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
320
-				|| $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
321
-			)
322
-			&& file_exists($transFile)) {
323
-			// load the translations file
324
-			$languageFiles[] = $transFile;
325
-		}
326
-
327
-		// merge with translations from theme
328
-		$theme = $this->config->getSystemValue('theme');
329
-		if (!empty($theme)) {
330
-			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
331
-			if (file_exists($transFile)) {
332
-				$languageFiles[] = $transFile;
333
-			}
334
-		}
335
-
336
-		return $languageFiles;
337
-	}
338
-
339
-	/**
340
-	 * find the l10n directory
341
-	 *
342
-	 * @param string $app App id or empty string for core
343
-	 * @return string directory
344
-	 */
345
-	protected function findL10nDir($app = null) {
346
-		if (in_array($app, ['core', 'lib', 'settings'])) {
347
-			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
348
-				return $this->serverRoot . '/' . $app . '/l10n/';
349
-			}
350
-		} else if ($app && \OC_App::getAppPath($app) !== false) {
351
-			// Check if the app is in the app folder
352
-			return \OC_App::getAppPath($app) . '/l10n/';
353
-		}
354
-		return $this->serverRoot . '/core/l10n/';
355
-	}
356
-
357
-
358
-	/**
359
-	 * Creates a function from the plural string
360
-	 *
361
-	 * Parts of the code is copied from Habari:
362
-	 * https://github.com/habari/system/blob/master/classes/locale.php
363
-	 * @param string $string
364
-	 * @return string
365
-	 */
366
-	public function createPluralFunction($string) {
367
-		if (isset($this->pluralFunctions[$string])) {
368
-			return $this->pluralFunctions[$string];
369
-		}
370
-
371
-		if (preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
372
-			// sanitize
373
-			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
374
-			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
375
-
376
-			$body = str_replace(
377
-				array( 'plural', 'n', '$n$plurals', ),
378
-				array( '$plural', '$n', '$nplurals', ),
379
-				'nplurals='. $nplurals . '; plural=' . $plural
380
-			);
381
-
382
-			// add parents
383
-			// important since PHP's ternary evaluates from left to right
384
-			$body .= ';';
385
-			$res = '';
386
-			$p = 0;
387
-			for($i = 0; $i < strlen($body); $i++) {
388
-				$ch = $body[$i];
389
-				switch ( $ch ) {
390
-					case '?':
391
-						$res .= ' ? (';
392
-						$p++;
393
-						break;
394
-					case ':':
395
-						$res .= ') : (';
396
-						break;
397
-					case ';':
398
-						$res .= str_repeat( ')', $p ) . ';';
399
-						$p = 0;
400
-						break;
401
-					default:
402
-						$res .= $ch;
403
-				}
404
-			}
405
-
406
-			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
407
-			$function = create_function('$n', $body);
408
-			$this->pluralFunctions[$string] = $function;
409
-			return $function;
410
-		} else {
411
-			// default: one plural form for all cases but n==1 (english)
412
-			$function = create_function(
413
-				'$n',
414
-				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
415
-			);
416
-			$this->pluralFunctions[$string] = $function;
417
-			return $function;
418
-		}
419
-	}
43
+    /** @var string */
44
+    protected $requestLanguage = '';
45
+
46
+    /**
47
+     * cached instances
48
+     * @var array Structure: Lang => App => \OCP\IL10N
49
+     */
50
+    protected $instances = [];
51
+
52
+    /**
53
+     * @var array Structure: App => string[]
54
+     */
55
+    protected $availableLanguages = [];
56
+
57
+    /**
58
+     * @var array Structure: string => callable
59
+     */
60
+    protected $pluralFunctions = [];
61
+
62
+    /** @var IConfig */
63
+    protected $config;
64
+
65
+    /** @var IRequest */
66
+    protected $request;
67
+
68
+    /** @var IUserSession */
69
+    protected $userSession;
70
+
71
+    /** @var string */
72
+    protected $serverRoot;
73
+
74
+    /**
75
+     * @param IConfig $config
76
+     * @param IRequest $request
77
+     * @param IUserSession $userSession
78
+     * @param string $serverRoot
79
+     */
80
+    public function __construct(IConfig $config,
81
+                                IRequest $request,
82
+                                IUserSession $userSession,
83
+                                $serverRoot) {
84
+        $this->config = $config;
85
+        $this->request = $request;
86
+        $this->userSession = $userSession;
87
+        $this->serverRoot = $serverRoot;
88
+    }
89
+
90
+    /**
91
+     * Get a language instance
92
+     *
93
+     * @param string $app
94
+     * @param string|null $lang
95
+     * @return \OCP\IL10N
96
+     */
97
+    public function get($app, $lang = null) {
98
+        $app = \OC_App::cleanAppId($app);
99
+        if ($lang !== null) {
100
+            $lang = str_replace(array('\0', '/', '\\', '..'), '', (string) $lang);
101
+        }
102
+
103
+        $forceLang = $this->config->getSystemValue('force_language', false);
104
+        if (is_string($forceLang)) {
105
+            $lang = $forceLang;
106
+        }
107
+
108
+        if ($lang === null || !$this->languageExists($app, $lang)) {
109
+            $lang = $this->findLanguage($app);
110
+        }
111
+
112
+        if (!isset($this->instances[$lang][$app])) {
113
+            $this->instances[$lang][$app] = new L10N(
114
+                $this, $app, $lang,
115
+                $this->getL10nFilesForApp($app, $lang)
116
+            );
117
+        }
118
+
119
+        return $this->instances[$lang][$app];
120
+    }
121
+
122
+    /**
123
+     * Find the best language
124
+     *
125
+     * @param string|null $app App id or null for core
126
+     * @return string language If nothing works it returns 'en'
127
+     */
128
+    public function findLanguage($app = null) {
129
+        if ($this->requestLanguage !== '' && $this->languageExists($app, $this->requestLanguage)) {
130
+            return $this->requestLanguage;
131
+        }
132
+
133
+        /**
134
+         * At this point Nextcloud might not yet be installed and thus the lookup
135
+         * in the preferences table might fail. For this reason we need to check
136
+         * whether the instance has already been installed
137
+         *
138
+         * @link https://github.com/owncloud/core/issues/21955
139
+         */
140
+        if($this->config->getSystemValue('installed', false)) {
141
+            $userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
142
+            if(!is_null($userId)) {
143
+                $userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
144
+            } else {
145
+                $userLang = null;
146
+            }
147
+        } else {
148
+            $userId = null;
149
+            $userLang = null;
150
+        }
151
+
152
+        if ($userLang) {
153
+            $this->requestLanguage = $userLang;
154
+            if ($this->languageExists($app, $userLang)) {
155
+                return $userLang;
156
+            }
157
+        }
158
+
159
+        try {
160
+            // Try to get the language from the Request
161
+            $lang = $this->getLanguageFromRequest($app);
162
+            // use formal version of german ("Sie" instead of "Du") by default
163
+            $lang = strtolower($lang) === 'de' ? 'de_DE' : $lang;
164
+            if ($userId !== null && $app === null && !$userLang) {
165
+                $this->config->setUserValue($userId, 'core', 'lang', $lang);
166
+            }
167
+            return $lang;
168
+        } catch (LanguageNotFoundException $e) {
169
+            // Finding language from request failed fall back to default language
170
+            $defaultLanguage = $this->config->getSystemValue('default_language', false);
171
+            if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
172
+                return $defaultLanguage;
173
+            }
174
+        }
175
+
176
+        // We could not find any language so fall back to english
177
+        return 'en';
178
+    }
179
+
180
+    /**
181
+     * Find all available languages for an app
182
+     *
183
+     * @param string|null $app App id or null for core
184
+     * @return array an array of available languages
185
+     */
186
+    public function findAvailableLanguages($app = null) {
187
+        $key = $app;
188
+        if ($key === null) {
189
+            $key = 'null';
190
+        }
191
+
192
+        // also works with null as key
193
+        if (!empty($this->availableLanguages[$key])) {
194
+            return $this->availableLanguages[$key];
195
+        }
196
+
197
+        $available = ['en']; //english is always available
198
+        $dir = $this->findL10nDir($app);
199
+        if (is_dir($dir)) {
200
+            $files = scandir($dir);
201
+            if ($files !== false) {
202
+                foreach ($files as $file) {
203
+                    if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
204
+                        $available[] = substr($file, 0, -5);
205
+                    }
206
+                }
207
+            }
208
+        }
209
+
210
+        // merge with translations from theme
211
+        $theme = $this->config->getSystemValue('theme');
212
+        if (!empty($theme)) {
213
+            $themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
214
+
215
+            if (is_dir($themeDir)) {
216
+                $files = scandir($themeDir);
217
+                if ($files !== false) {
218
+                    foreach ($files as $file) {
219
+                        if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
220
+                            $available[] = substr($file, 0, -5);
221
+                        }
222
+                    }
223
+                }
224
+            }
225
+        }
226
+
227
+        $this->availableLanguages[$key] = $available;
228
+        return $available;
229
+    }
230
+
231
+    /**
232
+     * @param string|null $app App id or null for core
233
+     * @param string $lang
234
+     * @return bool
235
+     */
236
+    public function languageExists($app, $lang) {
237
+        if ($lang === 'en') {//english is always available
238
+            return true;
239
+        }
240
+
241
+        $languages = $this->findAvailableLanguages($app);
242
+        return array_search($lang, $languages) !== false;
243
+    }
244
+
245
+    /**
246
+     * @param string|null $app
247
+     * @return string
248
+     * @throws LanguageNotFoundException
249
+     */
250
+    private function getLanguageFromRequest($app) {
251
+        $header = $this->request->getHeader('ACCEPT_LANGUAGE');
252
+        if ($header) {
253
+            $available = $this->findAvailableLanguages($app);
254
+
255
+            // E.g. make sure that 'de' is before 'de_DE'.
256
+            sort($available);
257
+
258
+            $preferences = preg_split('/,\s*/', strtolower($header));
259
+            foreach ($preferences as $preference) {
260
+                list($preferred_language) = explode(';', $preference);
261
+                $preferred_language = str_replace('-', '_', $preferred_language);
262
+
263
+                foreach ($available as $available_language) {
264
+                    if ($preferred_language === strtolower($available_language)) {
265
+                        return $available_language;
266
+                    }
267
+                }
268
+
269
+                // Fallback from de_De to de
270
+                foreach ($available as $available_language) {
271
+                    if (substr($preferred_language, 0, 2) === $available_language) {
272
+                        return $available_language;
273
+                    }
274
+                }
275
+            }
276
+        }
277
+
278
+        throw new LanguageNotFoundException();
279
+    }
280
+
281
+    /**
282
+     * Checks if $sub is a subdirectory of $parent
283
+     *
284
+     * @param string $sub
285
+     * @param string $parent
286
+     * @return bool
287
+     */
288
+    private function isSubDirectory($sub, $parent) {
289
+        // Check whether $sub contains no ".."
290
+        if(strpos($sub, '..') !== false) {
291
+            return false;
292
+        }
293
+
294
+        // Check whether $sub is a subdirectory of $parent
295
+        if (strpos($sub, $parent) === 0) {
296
+            return true;
297
+        }
298
+
299
+        return false;
300
+    }
301
+
302
+    /**
303
+     * Get a list of language files that should be loaded
304
+     *
305
+     * @param string $app
306
+     * @param string $lang
307
+     * @return string[]
308
+     */
309
+    // FIXME This method is only public, until OC_L10N does not need it anymore,
310
+    // FIXME This is also the reason, why it is not in the public interface
311
+    public function getL10nFilesForApp($app, $lang) {
312
+        $languageFiles = [];
313
+
314
+        $i18nDir = $this->findL10nDir($app);
315
+        $transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
316
+
317
+        if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
318
+                || $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
319
+                || $this->isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
320
+                || $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
321
+            )
322
+            && file_exists($transFile)) {
323
+            // load the translations file
324
+            $languageFiles[] = $transFile;
325
+        }
326
+
327
+        // merge with translations from theme
328
+        $theme = $this->config->getSystemValue('theme');
329
+        if (!empty($theme)) {
330
+            $transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
331
+            if (file_exists($transFile)) {
332
+                $languageFiles[] = $transFile;
333
+            }
334
+        }
335
+
336
+        return $languageFiles;
337
+    }
338
+
339
+    /**
340
+     * find the l10n directory
341
+     *
342
+     * @param string $app App id or empty string for core
343
+     * @return string directory
344
+     */
345
+    protected function findL10nDir($app = null) {
346
+        if (in_array($app, ['core', 'lib', 'settings'])) {
347
+            if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
348
+                return $this->serverRoot . '/' . $app . '/l10n/';
349
+            }
350
+        } else if ($app && \OC_App::getAppPath($app) !== false) {
351
+            // Check if the app is in the app folder
352
+            return \OC_App::getAppPath($app) . '/l10n/';
353
+        }
354
+        return $this->serverRoot . '/core/l10n/';
355
+    }
356
+
357
+
358
+    /**
359
+     * Creates a function from the plural string
360
+     *
361
+     * Parts of the code is copied from Habari:
362
+     * https://github.com/habari/system/blob/master/classes/locale.php
363
+     * @param string $string
364
+     * @return string
365
+     */
366
+    public function createPluralFunction($string) {
367
+        if (isset($this->pluralFunctions[$string])) {
368
+            return $this->pluralFunctions[$string];
369
+        }
370
+
371
+        if (preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
372
+            // sanitize
373
+            $nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
374
+            $plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
375
+
376
+            $body = str_replace(
377
+                array( 'plural', 'n', '$n$plurals', ),
378
+                array( '$plural', '$n', '$nplurals', ),
379
+                'nplurals='. $nplurals . '; plural=' . $plural
380
+            );
381
+
382
+            // add parents
383
+            // important since PHP's ternary evaluates from left to right
384
+            $body .= ';';
385
+            $res = '';
386
+            $p = 0;
387
+            for($i = 0; $i < strlen($body); $i++) {
388
+                $ch = $body[$i];
389
+                switch ( $ch ) {
390
+                    case '?':
391
+                        $res .= ' ? (';
392
+                        $p++;
393
+                        break;
394
+                    case ':':
395
+                        $res .= ') : (';
396
+                        break;
397
+                    case ';':
398
+                        $res .= str_repeat( ')', $p ) . ';';
399
+                        $p = 0;
400
+                        break;
401
+                    default:
402
+                        $res .= $ch;
403
+                }
404
+            }
405
+
406
+            $body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
407
+            $function = create_function('$n', $body);
408
+            $this->pluralFunctions[$string] = $function;
409
+            return $function;
410
+        } else {
411
+            // default: one plural form for all cases but n==1 (english)
412
+            $function = create_function(
413
+                '$n',
414
+                '$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
415
+            );
416
+            $this->pluralFunctions[$string] = $function;
417
+            return $function;
418
+        }
419
+    }
420 420
 }
Please login to merge, or discard this patch.