i18n: static pot to json compilation

Translations aren't embedded on every page load anymore;
this saves roughly 55kB on every single request, because
these can now be cached.
This commit is contained in:
Katharina Bogad 2021-06-20 23:12:17 +02:00
parent 64b8195f62
commit 7f598058bf
11 changed files with 162 additions and 13 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
/public/node_modules /public/node_modules
node_modules node_modules
.yarn .yarn
.vagrant
/vendor /vendor
/.release /.release
embedded.txt embedded.txt

8
Vagrantfile vendored Normal file
View File

@ -0,0 +1,8 @@
Vagrant.configure("2") do |config|
config.vm.box = "debian/buster64"
config.vm.provision "shell", path: "buildfiles/provision-vagrant.sh"
config.vm.network "forwarded_port", guest: 80, host: 8000
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.synced_folder ".", "/grocy", automount: true, owner: "www-data", group: "www-data"
end

View File

@ -0,0 +1,30 @@
<?php
use Grocy\Services\LocalizationService;
/* This file statically generates json to handle
* frontend translations.
*/
define("GROCY_DATAPATH", __DIR__ . '/../data');
// Load composer dependencies
require_once __DIR__ . '/../vendor/autoload.php';
// Load config files
require_once GROCY_DATAPATH . '/config.php';
require_once __DIR__ . '/../config-dist.php'; // For not in own config defined values we use the default ones
echo "Searching for localizations in " . __DIR__ . '/../localization/* \n';
$translations = array_filter(glob(__DIR__ . '/../localization/*'), 'is_dir');
// ensure the target directory is there
if(!is_dir(__DIR__ . '/../public/js/locales/grocy/')) {
mkdir(__DIR__ . '/../public/js/locales/grocy/', 0777, true);
}
foreach($translations as $lang) {
$culture = basename($lang);
echo "Generating " . $culture . "...\n";
$ls = LocalizationService::getInstance($culture, true);
$ls->LoadLocalizations(false);
file_put_contents(__DIR__ .'/../public/js/locales/grocy/'.$culture.'.json', $ls->GetPoAsJsonString());
}

22
buildfiles/grocy.nginx Normal file
View File

@ -0,0 +1,22 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
root /grocy/public;
index index.php;
server_name _;
location / {
try_files $uri $uri/ /index.php$is_args$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -0,0 +1,14 @@
apt update
# install php
apt -y install lsb-release apt-transport-https ca-certificates
wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php.list
apt update
apt -y install php7.4-fpm php7.4-cli php7.4-{bcmath,bz2,intl,gd,mbstring,zip,sqlite} nginx
cp /grocy/buildfiles/grocy.nginx /etc/nginx/sites-enabled/default
systemctl enable nginx.service
systemctl restart nginx.service

View File

@ -130,7 +130,14 @@ class BaseController
$this->View->set('__n', function ($number, $singularForm, $pluralForm) use ($localizationService) { $this->View->set('__n', function ($number, $singularForm, $pluralForm) use ($localizationService) {
return $localizationService->__n($number, $singularForm, $pluralForm); return $localizationService->__n($number, $singularForm, $pluralForm);
}); });
$this->View->set('GettextPo', $localizationService->GetPoAsJsonString()); $this->View->set('GrocyLocale', $localizationService->Culture);
// only inclue strings loaded from the database on every request,
// so that the rest can be cached and generated on build-time.
// TODO: figure out why this is needed in the first place.
$dynamicLocalizationService = new LocalizationService($localizationService->Culture, true);
$dynamicLocalizationService->LoadDynamicLocalizations();
$this->View->set('GettextPo', $dynamicLocalizationService->GetPoAsJsonString());
// TODO: Better handle this generically based on the current language (header in .po file?) // TODO: Better handle this generically based on the current language (header in .po file?)
$dir = 'ltr'; $dir = 'ltr';

View File

@ -152,7 +152,15 @@ function clean(cb)
function build(cb) function build(cb)
{ {
// body omitted // body omitted
return parallel(js, css, vendor, viewjs, resourceFileCopy, copyLocales, done => { done(); cb(); })(); return parallel(
js,
css,
vendor,
viewjs,
resourceFileCopy,
copyLocales,
makeLocales,
done => { done(); cb(); })();
} }
function publish(cb) function publish(cb)
@ -229,6 +237,11 @@ function resourceFileCopy(cb)
)(); )();
} }
async function makeLocales()
{
return subprocess.exec("php buildfiles/generate-locales.php");
}
function copyLocales(cb) function copyLocales(cb)
{ {
return parallel( return parallel(
@ -290,4 +303,5 @@ function bundle(cb)
.pipe(dest('.release')) .pipe(dest('.release'))
} }
export { build, js, vendor, viewjs, css, live, clean, resourceFileCopy, copyLocales, publish, release, bundle }
export { build, js, vendor, viewjs, css, live, clean, resourceFileCopy, copyLocales, publish, release, bundle, makeLocales }

View File

@ -44,12 +44,25 @@ class GrocyClass
this.EditObjectProduct = config.EditObjectProduct; this.EditObjectProduct = config.EditObjectProduct;
this.RecipePictureFileName = config.RecipePictureFileName; this.RecipePictureFileName = config.RecipePictureFileName;
this.InstructionManualFileNameName = config.InstructionManualFileNameName; this.InstructionManualFileNameName = config.InstructionManualFileNameName;
this.Locale = config.Locale;
this.Components = {}; this.Components = {};
// Init some classes // Init some classes
this.Api = new GrocyApi(this); this.Api = new GrocyApi(this);
this.Translator = new Translator(config.GettextPo);
// Merge dynamic and static locales
var strings = this.Api.LoadLanguageSync(this.Locale);
if (strings == null)
{
console.error("Could not load locale " + this.Locale + ", fallback to en.");
strings = this.Api.LoadLanguageSync("en");
}
Object.assign(strings.messages[""], config.GettextPo.messages[""]);
this.Translator = new Translator(strings);
this.FrontendHelpers = new GrocyFrontendHelpers(this, this.Api); this.FrontendHelpers = new GrocyFrontendHelpers(this, this.Api);
this.WakeLock = new WakeLock(this); this.WakeLock = new WakeLock(this);
this.UISound = new UISound(this); this.UISound = new UISound(this);

View File

@ -6,6 +6,28 @@ class GrocyApi
this.Grocy = Grocy; this.Grocy = Grocy;
} }
// This throws a deprecation warning in the console.
// The "clean" solution would be to move all translations
// To be promise-based async stuff, but Grocy not in a shape
// to make this easily possible right now.
// The introduction of a frontend framework like react or vue
// will make this obsolete as well.
LoadLanguageSync(locale)
{
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", this.Grocy.FormatUrl('/js/locales/grocy/' + locale + '.json'), false);
xmlhttp.send();
// if Status OK or NOT MODIFIED
if ((xmlhttp.status == 200 || xmlhttp.status == 304) && xmlhttp.readyState == 4)
{
return JSON.parse(xmlhttp.responseText);
}
else
{
return null;
}
}
Get(apiFunction, success, error) Get(apiFunction, success, error)
{ {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();

View File

@ -8,6 +8,8 @@ use Gettext\Translator;
class LocalizationService class LocalizationService
{ {
const DOMAIN = 'grocy/userstrings';
protected $Po; protected $Po;
protected $PoUserStrings; protected $PoUserStrings;
@ -62,11 +64,15 @@ class LocalizationService
return $this->Po->toJsonString(); return $this->Po->toJsonString();
} }
public function __construct(string $culture) public function __construct(string $culture, $deferLoading = false)
{ {
$this->Culture = $culture; $this->Culture = $culture;
$this->PoUserStrings = new Translations();
$this->PoUserStrings->setDomain(self::DOMAIN);
$this->LoadLocalizations($culture); if(!$deferLoading) {
$this->LoadLocalizations($culture);
}
} }
public function __n($number, $singularForm, $pluralForm) public function __n($number, $singularForm, $pluralForm)
@ -110,7 +116,7 @@ class LocalizationService
return $this->getDatabaseService()->GetDbConnection(); return $this->getDatabaseService()->GetDbConnection();
} }
private function LoadLocalizations() public function LoadLocalizations($includeDynamic = true)
{ {
$culture = $this->Culture; $culture = $this->Culture;
@ -133,9 +139,6 @@ class LocalizationService
} }
} }
$this->PoUserStrings = new Translations();
$this->PoUserStrings->setDomain('grocy/userstrings');
$this->Po = Translations::fromPoFile(__DIR__ . "/../localization/$culture/strings.po"); $this->Po = Translations::fromPoFile(__DIR__ . "/../localization/$culture/strings.po");
if (file_exists(__DIR__ . "/../localization/$culture/chore_assignment_types.po")) if (file_exists(__DIR__ . "/../localization/$culture/chore_assignment_types.po"))
@ -178,7 +181,18 @@ class LocalizationService
$this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/demo_data.po")); $this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/demo_data.po"));
} }
if($includeDynamic)
{
$this->LoadDynamicLocalizations();
}
$this->Translator = new Translator();
$this->Translator->loadTranslations($this->Po);
}
public function LoadDynamicLocalizations() {
$quantityUnits = null; $quantityUnits = null;
try try
{ {
$quantityUnits = $this->getDatabase()->quantity_units()->fetchAll(); $quantityUnits = $this->getDatabase()->quantity_units()->fetchAll();
@ -200,10 +214,13 @@ class LocalizationService
$this->PoUserStrings[] = $translation; $this->PoUserStrings[] = $translation;
} }
if($this->Po == null)
{
$this->Po = new Translations();
$this->Po->setDomain(self::DOMAIN);
}
$this->Po = $this->Po->mergeWith($this->PoUserStrings); $this->Po = $this->Po->mergeWith($this->PoUserStrings);
} }
$this->Translator = new Translator();
$this->Translator->loadTranslations($this->Po);
} }
} }

View File

@ -68,6 +68,7 @@
Currency:'{{ GROCY_CURRENCY }}', Currency:'{{ GROCY_CURRENCY }}',
CalendarFirstDayOfWeek: '{{ GROCY_CALENDAR_FIRST_DAY_OF_WEEK }}', CalendarFirstDayOfWeek: '{{ GROCY_CALENDAR_FIRST_DAY_OF_WEEK }}',
CalendarShowWeekNumbers: {{ BoolToString(GROCY_CALENDAR_SHOW_WEEK_OF_YEAR) }}, CalendarShowWeekNumbers: {{ BoolToString(GROCY_CALENDAR_SHOW_WEEK_OF_YEAR) }},
Locale: '{{ $GrocyLocale }}',
GettextPo: {!! $GettextPo !!}, GettextPo: {!! $GettextPo !!},
FeatureFlags: {!! json_encode($featureFlags) !!}, FeatureFlags: {!! json_encode($featureFlags) !!},
Webhooks: { Webhooks: {