Skip to content

Commit d1dbaa3

Browse files
authored
Implement cross-channel localization support, add Focus for iOS and Android (#895)
1 parent 28f2954 commit d1dbaa3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1246
-989
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.idea
22
.php_cs.cache
3+
*.pyc
34
app/config/config.ini
45
app/config/sources/*.json
56
app/config/sources/*.txt
@@ -14,9 +15,9 @@ logs/github_log.txt
1415
phpDocumentor.phar
1516
vendor
1617
web/assets
17-
web/p12n
1818
web/data.tar.gz
1919
web/docs
2020
web/download/.htaccess
2121
web/download/*.tmx
22+
web/p12n
2223
web/TMX

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Transvision is a Web application targeting the Mozilla localization community, c
55

66
The main purpose of Transvision is to provide a specialized search engine to find localized strings in Mozilla code repositories for all Mozilla products (Firefox, Thunderbird, Seamonkey) and websites (currently only www.mozilla.org is supported) via a Web interface. There are also side-features such as checks for common typographical errors for some languages, validity checks for localized access keys in the UI, or comparison views between Mozilla repository channels (Nightly/Beta/Release).
77

8-
Transvision is written in PHP, the string extraction is done with the Silme library (Python) and server install/maintenance scripts are in Bash.
8+
Transvision is written in PHP, the string extraction is done with the compare-locales library (Python) and server install/maintenance scripts are in Bash.
99

1010
Transvision is available at:
1111
https://transvision.mozfr.org
@@ -15,8 +15,6 @@ https://transvision-beta.mozfr.org
1515

1616
Transvision was created by Philippe Dessante, from the French Mozilla localization team.
1717

18-
Lead developer since version 1.0 : Pascal Chevrel (pascal AT mozilla DOT com).
19-
2018
## Getting Started
2119

2220
The Transvision team uses Git and GitHub for both development and issue tracking.

app/classes/Transvision/API.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* Calls are like this:
1616
* api/<version>/<service>/<repository>/<search type>/<source locale>/<target locale>/<url escaped search>/?optional_parameter1=foo&optional_parameter2=bar
1717
* Example for an entity search containing bookmark:
18-
* https://transvision.mozfr.org/api/v1/tm/release/entity/en-US/fr/bookmark/?case_sensitive=case_sensitive
18+
* https://transvision.mozfr.org/api/v1/tm/gecko_strings/entity/en-US/fr/bookmark/?case_sensitive=case_sensitive
1919
* (tm = translation memory service)
2020
*
2121
* Example for the list of locales supported for a repo:
@@ -212,7 +212,7 @@ private function isValidServiceCall($service)
212212

213213
break;
214214
case 'locales':
215-
// ex: /api/v1/locales/release/
215+
// ex: /api/v1/locales/gecko_strings/
216216
if (! $this->verifyEnoughParameters(3)) {
217217
return false;
218218
}
@@ -276,10 +276,10 @@ private function isValidServiceCall($service)
276276
case 'suggestions':
277277
/*
278278
Use the same settings as 'tm'
279-
ex: /api/v1/suggestions/release/en-US/fr/string/Home%20page/?max_results=3
279+
ex: /api/v1/suggestions/gecko_strings/en-US/fr/string/Home%20page/?max_results=3
280280
*/
281281
case 'tm':
282-
// ex: /api/v1/tm/release/en-US/fr/string/Home%20page/?max_results=3&min_quality=80
282+
// ex: /api/v1/tm/gecko_strings/en-US/fr/string/Home%20page/?max_results=3&min_quality=80
283283
if (! $this->verifyEnoughParameters(6)) {
284284
return false;
285285
}

app/classes/Transvision/AnalyseStrings.php

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,18 @@ public static function differences($tmx_source, $tmx_target, $repo, $ignored_str
4545
{
4646
$pattern_mismatch = [];
4747

48-
switch ($repo) {
49-
case 'firefox_ios':
50-
$patterns = [
51-
'ios' => '/(%(?:[0-9]+\$){0,1}@)/i', // %@, but also %1$@, %2$@, etc.
52-
];
53-
break;
54-
default:
55-
$patterns = [
56-
'dtd' => '/&([a-z0-9\.]+);/i', // &foobar;
57-
'printf' => '/(%(?:[0-9]+\$){0,1}(?:[0-9].){0,1}(S))/i', // %1$S or %S. %1$0.S and %0.S are valid too
58-
'properties' => '/(?<!%[0-9])\$[a-z0-9\.]+\b/i', // $BrandShortName, but not "My%1$SFeeds-%2$S.opml"
59-
'l10njs' => '/\{\{\s*([a-z0-9_]+)\s*\}\}/iu', // {{foobar2}} Used in Loop and PDFViewer
60-
];
61-
break;
62-
}
48+
$patterns = [
49+
'dtd' => '/&([A-Za-z0-9\.]+);/', // &foobar;
50+
'printf' => '/(%(?:[0-9]+\$){0,1}(?:[0-9].){0,1}([sS]))/', // %1$S or %S. %1$0.S and %0.S are valid too
51+
'properties' => '/(?<!%[0-9])\$[A-Za-z0-9\.]+\b/', // $BrandShortName, but not "My%1$SFeeds-%2$S.opml"
52+
'l10njs' => '/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/u', // {{foobar2}} Used in Loop and PDFViewer
53+
'ios' => '/(%(?:[0-9]+\$){0,1}@)/i', // %@, but also %1$@, %2$@, etc.
54+
];
55+
$repo_patterns = Project::$repos_info[$repo]['variable_patterns'];
56+
57+
$patterns = array_filter($patterns, function($k) use ($repo_patterns) {
58+
return in_array($k, $repo_patterns);
59+
}, ARRAY_FILTER_USE_KEY);
6360

6461
foreach ($patterns as $pattern_name => $pattern) {
6562
foreach ($tmx_source as $key => $source) {

app/classes/Transvision/Dotlang.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,6 @@ public static function getStrings($file, $reference_locale)
137137
*/
138138
public static function generateStringID($file_path, $string)
139139
{
140-
return $file_path . ':' . hash('crc32', $string);
140+
return $file_path . ':' . hash('md5', $string);
141141
}
142142
}

app/classes/Transvision/Files.php

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,24 @@ public static function fileForceContents($dir, $contents)
3636
}
3737

3838
/**
39-
* Return the list of files in a folder as an array.
40-
* Hidden files starting with a dot (.svn, .htaccess...) are ignored.
39+
* Return the list of folders in a path as an array.
40+
* Hidden folders starting with a dot (.svn, .htaccess...) are ignored.
4141
*
42-
* @param string $folder the directory we want to scan
43-
* @param array $excluded_files Files to exclude from results
42+
* @param string $path The directory we want to scan
43+
* @param array $excluded_folders Folders to exclude from results
4444
*
45-
* @return array Files in the folder
45+
* @return array Folders in path
4646
*/
47-
public static function getFilenamesInFolder($folder, $excluded_files = [])
47+
public static function getFoldersInPath($path, $excluded_folders = [])
4848
{
49-
/*
50-
Here we exclude by default hidden files starting with a dot and
51-
the . and .. symbols for directories
52-
*/
53-
$files = array_filter(
54-
scandir($folder),
55-
function ($item) {
56-
return !Strings::startsWith($item, '.');
49+
// We exclude by default hidden folders starting with a dot
50+
$folders = array_filter(
51+
scandir($path),
52+
function ($item) use ($path) {
53+
return is_dir("{$path}/{$item}") && ! Strings::startsWith($item, '.');
5754
}
5855
);
5956

60-
return array_diff($files, $excluded_files);
57+
return array_diff($folders, $excluded_folders);
6158
}
6259
}

app/classes/Transvision/Po.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
namespace Transvision;
3+
4+
use Gettext\Translations;
5+
6+
/**
7+
* Po class
8+
*
9+
* This class is used to manipulate translation files in Gettext (.po) format.
10+
*
11+
* @package Transvision
12+
*/
13+
class Po
14+
{
15+
/**
16+
*
17+
* Loads strings from a .po file
18+
*
19+
* @param string $po_path Path to the .po to load
20+
* @param string $file_name Name of the file extracted
21+
* @param string $project_name The project this string belongs to
22+
* @param boolean $template If I'm looking at templates
23+
*
24+
* @return array Array of strings as [string_id => translation]
25+
*/
26+
public static function getStrings($po_path, $file_name, $project_name, $template = false)
27+
{
28+
$translations = Translations::fromPoFile($po_path);
29+
$strings = [];
30+
31+
foreach ($translations as $translation_obj) {
32+
$translated_string = $translation_obj->getTranslation();
33+
34+
// Ignore fuzzy strings
35+
if (in_array('fuzzy', $translation_obj->getFlags())) {
36+
continue;
37+
}
38+
39+
// Ignore empty (untranslated) strings
40+
if ($translated_string == '' && ! $template) {
41+
continue;
42+
}
43+
44+
// In templates, use the original string as translation
45+
if ($template) {
46+
$translated_string = $translation_obj->getOriginal();
47+
}
48+
49+
$string_id = self::generateStringID(
50+
$project_name,
51+
$file_name,
52+
$translation_obj->getContext() . '-' . $translation_obj->getOriginal()
53+
);
54+
$translated_string = str_replace("'", "\\'", $translated_string);
55+
$strings[$string_id] = $translated_string;
56+
57+
// Check if there are plurals, in case put them as translation of
58+
// the only English plural form
59+
if ($translation_obj->hasPluralTranslations()) {
60+
$string_id = self::generateStringID(
61+
$project_name,
62+
$file_name,
63+
$translation_obj->getContext() . '-' . $translation_obj->getPlural()
64+
);
65+
$translated_string = implode("\n", $translation_obj->getPluralTranslations());
66+
$translated_string = str_replace("'", "\\'", $translated_string);
67+
$strings[$string_id] = $translated_string;
68+
}
69+
}
70+
71+
return $strings;
72+
}
73+
74+
/**
75+
* Generate a unique ID for a string to store in Transvision.
76+
*
77+
* @param string $project_name The project this string belongs to
78+
* @param string $file_name .po file name
79+
* @param string $string_id String ID (context-original text)
80+
*
81+
* @return string unique ID such as focus_android/app.po:1dafea7725862ca854c408f0e2df9c88
82+
*/
83+
public static function generateStringID($project_name, $file_name, $string_id)
84+
{
85+
return "{$project_name}/{$file_name}:" . hash('md5', $string_id);
86+
}
87+
}

app/classes/Transvision/Project.php

Lines changed: 108 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,99 @@ class Project
2525
'calendar' => 'Lightning',
2626
];
2727

28+
/*
29+
* This array contains information about supported repositories.
30+
*
31+
* files: list of files to analyze during extraction
32+
*
33+
* git_repository: name of remote Git repository in the mozilla-l10n org
34+
*
35+
* git_subfolder: if localizations are in a subdirectory, e.g. if they're
36+
* subfolders in /locales, value will be simply "locales" (no ending or
37+
* starting /)
38+
*
39+
* locale_mapping: if locale codes need to be mapped (Mozilla code -> Repo code)
40+
*
41+
* pontoon_project: name of the project in Pontoon
42+
*
43+
* source_type: source type used by the project (xliff, gettext, etc.)
44+
*
45+
* variable_patterns: list of patterns used to check for errors in variables.
46+
* Actual patterns (regex) are defined in the AnalyseStrings class
47+
*
48+
* @var array
49+
*
50+
*/
51+
public static $repos_info = [
52+
'firefox_ios' => [
53+
'files' => [
54+
'firefox-ios.xliff',
55+
],
56+
'git_repository' => 'firefoxios-l10n',
57+
'locale_mapping' => [
58+
'bn-IN' => 'bn',
59+
'bn-BD' => 'bn',
60+
'es-ES' => 'es',
61+
'son' => 'ses',
62+
],
63+
'pontoon_project' => 'firefox-for-ios',
64+
'source_type' => 'xliff',
65+
'variable_patterns' => ['ios'],
66+
],
67+
'focus_android' => [
68+
'files' => [
69+
'app.po',
70+
],
71+
'git_repository' => 'focus-android-l10n',
72+
'git_subfolder' => 'locales',
73+
'pontoon_project' => 'focus-for-android',
74+
'source_type' => 'gettext',
75+
'variable_patterns' => ['l10njs', 'printf'],
76+
],
77+
'focus_ios' => [
78+
'files' => [
79+
'focus-ios.xliff',
80+
],
81+
'git_repository' => 'focusios-l10n',
82+
'locale_mapping' => [
83+
'bn-IN' => 'bn',
84+
'bn-BD' => 'bn',
85+
'son' => 'ses',
86+
],
87+
'pontoon_project' => 'focus-for-ios',
88+
'source_type' => 'xliff',
89+
'variable_patterns' => ['ios'],
90+
],
91+
'gecko_strings'=> [
92+
'source_type' => 'mixed',
93+
'variable_patterns' => ['dtd', 'l10njs', 'printf', 'properties'],
94+
],
95+
'mozilla_org'=> [
96+
'git_repository' => 'www.mozilla.org',
97+
'pontoon_project' => 'mozillaorg',
98+
'source_type' => 'dotlang',
99+
],
100+
];
101+
102+
/*
103+
Since Project is used statically, not as an object, it would be too
104+
expensive to generate the list of repos dinamically from $repos_info.
105+
*/
106+
public static $repos_lists = [
107+
// Desktop products
108+
'desktop' => [
109+
'gecko_strings',
110+
],
111+
// Products using Git
112+
'git' => [
113+
'firefox_ios', 'focus_android', 'focus_ios', 'mozilla_org',
114+
],
115+
// Products using free text search on Pontoon
116+
'text_search' => [
117+
'firefox_ios', 'focus_android', 'focus_ios', 'mozilla_org',
118+
],
119+
];
120+
28121
/**
29122
* Create a list of all supported repositories.
30123
*
@@ -56,7 +149,10 @@ public static function getSupportedRepositories()
56149
*/
57150
public static function getRepositories()
58151
{
59-
return array_keys(self::getSupportedRepositories());
152+
$repositories = array_keys(self::getSupportedRepositories());
153+
sort($repositories);
154+
155+
return $repositories;
60156
}
61157

62158
/**
@@ -78,10 +174,7 @@ public static function getRepositoriesNames()
78174
*/
79175
public static function getDesktopRepositories()
80176
{
81-
return array_diff(
82-
self::getRepositories(),
83-
['mozilla_org', 'firefox_ios']
84-
);
177+
return self::$repos_lists['desktop'];
85178
}
86179

87180
/**
@@ -196,25 +289,17 @@ public static function getLocaleInContext($locale, $context)
196289
'sr-Latn' => 'sr',
197290
];
198291

199-
// Firefox for iOS
200-
$locale_mappings['firefox_ios'] = [
201-
'bn-IN' => 'bn',
202-
'bn-BD' => 'bn',
203-
'es-AR' => 'es',
204-
'es-ES' => 'es',
205-
'son' => 'ses',
206-
];
207-
208-
// For other contexts use the same as Bugzilla
209-
$locale_mappings['other'] = $locale_mappings['bugzilla'];
210-
211-
// Fallback to 'other' if context doesn't exist in $locale_mappings
212-
$context = array_key_exists($context, $locale_mappings)
213-
? $context
214-
: 'other';
292+
// Fall back to Bugzilla if there are no mappings for the requested context
293+
if (isset(self::$repos_info[$context]['locale_mapping'])) {
294+
$mapping = self::$repos_info[$context]['locale_mapping'];
295+
} elseif (isset($locale_mappings[$context])) {
296+
$mapping = $locale_mappings[$context];
297+
} else {
298+
$mapping = $locale_mappings['bugzilla'];
299+
}
215300

216-
$locale = array_key_exists($locale, $locale_mappings[$context])
217-
? $locale_mappings[$context][$locale]
301+
$locale = array_key_exists($locale, $mapping)
302+
? $mapping[$locale]
218303
: $locale;
219304

220305
return $locale;

0 commit comments

Comments
 (0)