"
+ generate_preferences_switch_button(lang().classify_products_according_to_your_preferences, "preferences_switch_in_preferences")
+ '' + lang().reset_preferences + ''
diff --git a/lib/ProductOpener/Attributes.pm b/lib/ProductOpener/Attributes.pm
index d6183d9272699..0fb4b8fba081c 100644
--- a/lib/ProductOpener/Attributes.pm
+++ b/lib/ProductOpener/Attributes.pm
@@ -61,13 +61,15 @@ use vars @EXPORT_OK;
use ProductOpener::Config qw/:all/;
use ProductOpener::Store qw/:all/;
-use ProductOpener::Tags qw/%level display_taxonomy_tag display_taxonomy_tag_name has_tag/;
+use ProductOpener::Tags
+ qw/%level display_taxonomy_tag display_taxonomy_tag_name has_tag get_inherited_property_from_tags/;
use ProductOpener::Products qw/:all/;
use ProductOpener::Food qw/@nutrient_levels/;
use ProductOpener::Ingredients qw/:all/;
use ProductOpener::Lang qw/f_lang_in_lc lang lang_in_other_lc/;
use ProductOpener::Display qw/$static_subdomain/;
use ProductOpener::Ecoscore qw/:all/;
+use ProductOpener::ProductsFeatures qw/feature_enabled/;
use Data::DeepAccess qw(deep_get);
@@ -96,9 +98,15 @@ $options{attribute_groups} = [
# Build a hash of attribute groups to make it easier to retrieve all attributes of a specific group
my %attribute_groups = ();
+# Build a hash of attributes to make it easier to retrieve all attributes
+my %attributes = ();
+
if (defined $options{attribute_groups}) {
foreach my $attribute_group_ref (@{$options{attribute_groups}}) {
$attribute_groups{$attribute_group_ref->[0]} = $attribute_group_ref->[1];
+ foreach my $attribute_id (@{$attribute_group_ref->[1]}) {
+ $attributes{$attribute_id} = 1;
+ }
}
}
@@ -306,6 +314,9 @@ sub initialize_attribute ($attribute_id, $target_lc) {
$attribute_ref->{icon_url} = "$static_subdomain/images/attributes/dist/${tag}.svg";
}
+ elsif ($attribute_id eq "repairability_index_france") {
+ $attribute_ref->{icon_url} = "$static_subdomain/images/lang/fr/labels/indice-de-reparabilite-10.152x90.svg";
+ }
# Initialize name and setting name if a language is requested
@@ -1656,57 +1667,68 @@ sub compute_attributes ($product_ref, $target_lc, $target_cc, $options_ref) {
# Nutritional quality
- $attribute_ref = compute_attribute_nutriscore($product_ref, $target_lc, $target_cc);
- add_attribute_to_group($product_ref, $target_lc, "nutritional_quality", $attribute_ref);
-
- foreach my $nutrient ("salt", "fat", "sugars", "saturated-fat") {
- $attribute_ref = compute_attribute_nutrient_level($product_ref, $target_lc, "low", $nutrient);
+ if (defined $attribute_groups{"nutritional_quality"}) {
+ $attribute_ref = compute_attribute_nutriscore($product_ref, $target_lc, $target_cc);
add_attribute_to_group($product_ref, $target_lc, "nutritional_quality", $attribute_ref);
+
+ foreach my $nutrient ("salt", "fat", "sugars", "saturated-fat") {
+ $attribute_ref = compute_attribute_nutrient_level($product_ref, $target_lc, "low", $nutrient);
+ add_attribute_to_group($product_ref, $target_lc, "nutritional_quality", $attribute_ref);
+ }
}
# Allergens
- foreach my $allergen_attribute_id (@{$attribute_groups{"allergens"}}) {
- $attribute_ref = compute_attribute_allergen($product_ref, $target_lc, $allergen_attribute_id);
- add_attribute_to_group($product_ref, $target_lc, "allergens", $attribute_ref);
+ if (defined $attribute_groups{"allergens"}) {
+ foreach my $allergen_attribute_id (@{$attribute_groups{"allergens"}}) {
+ $attribute_ref = compute_attribute_allergen($product_ref, $target_lc, $allergen_attribute_id);
+ add_attribute_to_group($product_ref, $target_lc, "allergens", $attribute_ref);
+ }
}
# Ingredients analysis
- foreach my $analysis ("vegan", "vegetarian", "palm-oil-free") {
- $attribute_ref = compute_attribute_ingredients_analysis($product_ref, $target_lc, $analysis);
- add_attribute_to_group($product_ref, $target_lc, "ingredients_analysis", $attribute_ref);
+ if (defined $attribute_groups{"ingredients_analysis"}) {
+ foreach my $analysis ("vegan", "vegetarian", "palm-oil-free") {
+ $attribute_ref = compute_attribute_ingredients_analysis($product_ref, $target_lc, $analysis);
+ add_attribute_to_group($product_ref, $target_lc, "ingredients_analysis", $attribute_ref);
+ }
}
# Processing
- $attribute_ref = compute_attribute_nova($product_ref, $target_lc);
- add_attribute_to_group($product_ref, $target_lc, "processing", $attribute_ref);
+ if (defined $attribute_groups{"processing"}) {
+ $attribute_ref = compute_attribute_nova($product_ref, $target_lc);
+ add_attribute_to_group($product_ref, $target_lc, "processing", $attribute_ref);
- $attribute_ref = compute_attribute_additives($product_ref, $target_lc);
- add_attribute_to_group($product_ref, $target_lc, "processing", $attribute_ref);
+ $attribute_ref = compute_attribute_additives($product_ref, $target_lc);
+ add_attribute_to_group($product_ref, $target_lc, "processing", $attribute_ref);
+ }
# Environment
- if ( (not defined $options_ref)
- or (not defined $options_ref->{skip_ecoscore})
- or (not $options_ref->{skip_ecoscore}))
- {
+ if (feature_enabled("ecoscore")) {
$attribute_ref = compute_attribute_ecoscore($product_ref, $target_lc, $target_cc);
add_attribute_to_group($product_ref, $target_lc, "environment", $attribute_ref);
}
- if ( (not defined $options_ref)
- or (not defined $options_ref->{skip_forest_footprint})
- or (not $options_ref->{skip_forest_footprint}))
- {
+ if (feature_enabled("forest_footprint")) {
$attribute_ref = compute_attribute_forest_footprint($product_ref, $target_lc);
add_attribute_to_group($product_ref, $target_lc, "environment", $attribute_ref);
}
+ if (defined $attributes{"repairability_index_france"}) {
+ $attribute_ref = compute_attribute_repairability_index_france($product_ref, $target_lc, $target_cc);
+ add_attribute_to_group($product_ref, $target_lc, "environment", $attribute_ref);
+ }
+
# Labels groups
- foreach my $label_id ("en:organic", "en:fair-trade") {
+ if (defined $attributes{"labels_organic"}) {
+ $attribute_ref = compute_attribute_has_tag($product_ref, $target_lc, "labels", "en:organic");
+ add_attribute_to_group($product_ref, $target_lc, "labels", $attribute_ref);
+ }
- $attribute_ref = compute_attribute_has_tag($product_ref, $target_lc, "labels", $label_id);
+ if (defined $attributes{"labels_fair_trade"}) {
+ $attribute_ref = compute_attribute_has_tag($product_ref, $target_lc, "labels", "en:fair-trade");
add_attribute_to_group($product_ref, $target_lc, "labels", $attribute_ref);
}
@@ -1721,4 +1743,129 @@ sub compute_attributes ($product_ref, $target_lc, $target_cc, $options_ref) {
return;
}
+=head2 compute_attribute_repairability_index_france ( $product_ref, $target_lc, $target_cc )
+
+Compute the repairability index attribute for France.
+
+=head3 Arguments
+
+=head4 product reference $product_ref
+
+Loaded from the MongoDB database, Storable files, or the OFF API.
+
+=head4 language code $target_lc
+
+Returned attributes contain both data and strings intended to be displayed to users.
+
+=head4 country code $target_cc
+
+The repairability index is specific to France.
+
+=head3 Return value
+
+The return value is a reference to the resulting attribute data structure.
+
+=head4 % Match
+
+- 10x the repairability index value (from 0 to 10)
+- 0% if the product does not have a repairability index value
+
+=cut
+
+sub compute_attribute_repairability_index_france ($product_ref, $target_lc, $target_cc) {
+
+ $log->debug("compute repairability index attribute",
+ {code => $product_ref->{code}, ecoscore_data => $product_ref->{labels_tags}})
+ if $log->is_debug();
+
+ my $attribute_id = "repairability_index_france";
+
+ my $attribute_ref = initialize_attribute($attribute_id, $target_lc);
+
+ $attribute_ref->{status} = "unknown";
+
+ # Check if the product has a label indicating the repairability index
+ # with a repairability_index_france_value:en: property
+
+ my ($value, $label_tag)
+ = get_inherited_property_from_tags("labels", $product_ref->{labels_tags},
+ "repairability_index_france_value:en");
+ if (defined $value) {
+ $attribute_ref->{status} = "known";
+ my $value_dash = $value;
+ $value_dash =~ s/\./-/;
+ # Compute match based on the repairability index value (from 0 to 10)
+ $attribute_ref->{match} = $value * 10;
+ $attribute_ref->{icon_url}
+ = "$static_subdomain/images/lang/fr/labels/indice-de-reparabilite-$value_dash.152x90.svg";
+ if ($target_lc ne "data") {
+ $attribute_ref->{title} = display_taxonomy_tag($target_lc, "labels", $label_tag);
+ my $value_description = "bad";
+ if ($value >= 8) {
+ $value_description = "very_good";
+ }
+ elsif ($value >= 6) {
+ $value_description = "good";
+ }
+ elsif ($value >= 4) {
+ $value_description = "average";
+ }
+ elsif ($value >= 2) {
+ $value_description = "bad";
+ }
+ $attribute_ref->{description_short}
+ = lang_in_other_lc($target_lc,
+ "attribute_repairability_index_france_" . $value_description . "_description_short");
+
+ }
+ }
+ # Check if the product is in an applicable category
+ # (smartphones, laptops, electric lawn mowers, dishwashers, vacuum cleaners and high-pressure cleaners)
+ # https://www.ecologie.gouv.fr/politiques-publiques/indice-reparabilite#lobjectif-de-lindice-0
+ elsif (
+ not(
+ (defined $product_ref->{categories_tags}) and (
+ scalar(
+ grep {
+ $_
+ =~ /en:(smartphones|laptops|electric-lawn-mowers|dishwashers|vacuum-cleaners|high-pressure-cleaners)/
+ } @{$product_ref->{categories_tags}}
+ )
+ )
+ )
+ )
+ {
+ $attribute_ref->{icon_url}
+ = "$static_subdomain/images/lang/fr/labels/indice-de-reparabilite-non-applicable.152x90.svg";
+ if ($target_lc ne "data") {
+ $attribute_ref->{title}
+ = lang_in_other_lc($target_lc, "attribute_repairability_index_france_not_applicable_title");
+ $attribute_ref->{description_short}
+ = lang_in_other_lc($target_lc, "attribute_repairability_index_france_not_applicable_description_short");
+ $attribute_ref->{description} = f_lang_in_lc(
+ $target_lc,
+ "f_attribute_repairability_index_france_not_applicable_description",
+ {
+ categories => join(',',
+ map {display_taxonomy_tag($target_lc, "categories", $_)} "en:smartphones",
+ "en:laptops", "en:electric-lawn-mowers", "en:dishwashers",
+ "en:vacuum-cleaners", "en:high-pressure-cleaners")
+ }
+ );
+ }
+ }
+ else {
+ $attribute_ref->{icon_url}
+ = "$static_subdomain/images/lang/fr/labels/indice-de-reparabilite-inconnu.152x90.svg";
+ if ($target_lc ne "data") {
+ $attribute_ref->{title}
+ = lang_in_other_lc($target_lc, "attribute_repairability_index_france_unknown_title");
+ $attribute_ref->{description_short}
+ = lang_in_other_lc($target_lc, "attribute_repairability_index_france_unknown_description_short");
+ }
+ }
+
+ return $attribute_ref;
+}
+
1;
diff --git a/lib/ProductOpener/Config_obf.pm b/lib/ProductOpener/Config_obf.pm
index 929b3fd8b8d57..86addf91a26c0 100644
--- a/lib/ProductOpener/Config_obf.pm
+++ b/lib/ProductOpener/Config_obf.pm
@@ -183,6 +183,19 @@ $flavor = 'obf';
ios_app_link => "https://apps.apple.com/app/open-beauty-facts/id1122926380?utm_source=obf&utf_medium=web",
facebook_page_url => "https://www.facebook.com/openbeautyfacts?utm_source=obf&utf_medium=web",
twitter_account => "OpenBeautyFacts",
+ # favicon HTML and images generated with https://realfavicongenerator.net/ using the SVG icon
+ favicons => <
+
+
+
+
+
+
+
+
+HTML
+ ,
);
$options{export_limit} = 10000;
@@ -484,6 +497,21 @@ HTML
last_image_t
);
+# Used to generate the list of possible product attributes, which is
+# used to display the possible choices for user preferences
+$options{attribute_groups}
+ = [["ingredients_analysis", ["vegan", "palm_oil_free",]], ["labels", ["labels_organic", "labels_fair_trade"]],];
+
+# default preferences for attributes
+$options{attribute_default_preferences} = {
+ "labels_organic" => "important",
+ "labels_fair_trade" => "important",
+};
+
+use JSON::MaybeXS;
+$options{attribute_default_preferences_json}
+ = JSON->new->utf8->canonical->encode($options{attribute_default_preferences});
+
# for ingredients OCR, we use tesseract-ocr
# on debian, dictionaries are in /usr/share/tesseract-ocr/tessdata
# %tesseract_ocr_available_languages provides mapping between OFF 2 letter language codes
diff --git a/lib/ProductOpener/Config_off.pm b/lib/ProductOpener/Config_off.pm
index f0ba69f6f433c..b6ee9c7f01037 100644
--- a/lib/ProductOpener/Config_off.pm
+++ b/lib/ProductOpener/Config_off.pm
@@ -204,6 +204,19 @@ $flavor = 'off';
facebook_page_url_fr => "https://www.facebook.com/OpenFoodFacts.fr",
twitter_account => "OpenFoodFacts",
twitter_account_fr => "OpenFoodFactsFr",
+ # favicon HTML and images generated with https://realfavicongenerator.net/ using the SVG icon
+ favicons => <
+
+
+
+
+
+
+
+
+HTML
+ ,
);
$options{export_limit} = 10000;
@@ -545,22 +558,6 @@ $options{manifest} = $manifest;
$options{display_random_sample_of_products_after_edits} = 0; # from MongoDB 3.2 onward
-$options{favicons} = <
-
-
-
-
-
-
-
-
-
-
-
-HTML
- ;
-
$options{opensearch_image} = <https://static.$server_domain/images/favicon/favicon.ico
XML
@@ -993,6 +990,10 @@ $options{attribute_default_preferences} = {
"ecoscore" => "important",
};
+use JSON::MaybeXS;
+$options{attribute_default_preferences_json}
+ = JSON->new->utf8->canonical->encode($options{attribute_default_preferences});
+
# Used to generate the sample import file for the producers platform
# possible values: mandatory, recommended, optional.
# when not specified, fields are considered optional
diff --git a/lib/ProductOpener/Config_opf.pm b/lib/ProductOpener/Config_opf.pm
index 54c049c97078e..8ae929e532a07 100644
--- a/lib/ProductOpener/Config_opf.pm
+++ b/lib/ProductOpener/Config_opf.pm
@@ -174,13 +174,26 @@ $flavor = "opf";
%options = (
site_name => "Open Products Facts",
- product_type => "products",
+ product_type => "product",
og_image_url => "https://world.openproductsfacts.org/images/misc/openproductsfacts-logo-en.png",
#android_apk_app_link => "https://world.openbeautyfacts.org/images/apps/obf.apk?utm_source=opf&utf_medium=web",
#android_app_link => "https://play.google.com/store/apps/details?id=org.openbeautyfacts.scanner&utm_source=opf&utf_medium=web",
#ios_app_link => "https://apps.apple.com/app/open-beauty-facts/id1122926380?utm_source=opf&utf_medium=web",
#facebook_page_url => "https://www.facebook.com/openbeautyfacts?&utm_source=opf&utf_medium=web",
#twitter_account => "OpenBeautyFacts",
+ # favicon HTML and images generated with https://realfavicongenerator.net/ using the SVG icon
+ favicons => <
+
+
+
+
+
+
+
+
+HTML
+ ,
);
$options{export_limit} = 10000;
@@ -305,20 +318,17 @@ HTML
@taxonomy_fields = qw(
units
languages states countries
- allergens origins additives_classes ingredients
+ origins
packaging_shapes packaging_materials packaging_recycling packaging
- labels food_groups categories
- ingredients_processing
- additives vitamins minerals amino_acids nucleotides other_nutritional_substances traces
- ingredients_analysis
- nutrients nutrient_levels misc nova_groups
+ labels categories
+ misc
periods_after_opening
data_quality data_quality_bugs data_quality_info data_quality_warnings data_quality_errors data_quality_warnings_producers data_quality_errors_producers
improvements
);
# tag types (=facets) that should be indexed by web crawlers, all other tag types are not indexable
-@index_tag_types = qw(brands categories labels additives nova_groups ecoscore nutrition_grades products);
+@index_tag_types = qw(brands categories labels products);
# fields in product edit form, above ingredients and nutrition facts
@@ -481,6 +491,22 @@ HTML
last_image_t
);
+# Used to generate the list of possible product attributes, which is
+# used to display the possible choices for user preferences
+$options{attribute_groups}
+ = [["labels", ["labels_organic", "labels_fair_trade"]], ["environment", ["repairability_index_france",]],];
+
+# default preferences for attributes
+$options{attribute_default_preferences} = {
+ "labels_organic" => "important",
+ "labels_fair_trade" => "important",
+ "repairability_index_france" => "important",
+};
+
+use JSON::MaybeXS;
+$options{attribute_default_preferences_json}
+ = JSON->new->utf8->canonical->encode($options{attribute_default_preferences});
+
# for ingredients OCR, we use tesseract-ocr
# on debian, dictionaries are in /usr/share/tesseract-ocr/tessdata
# %tesseract_ocr_available_languages provides mapping between OFF 2 letter language codes
diff --git a/lib/ProductOpener/Config_opff.pm b/lib/ProductOpener/Config_opff.pm
index 310435fb01bf1..02554b1b19f63 100644
--- a/lib/ProductOpener/Config_opff.pm
+++ b/lib/ProductOpener/Config_opff.pm
@@ -181,6 +181,21 @@ $flavor = "opff";
#ios_app_link => "https://apps.apple.com/app/open-beauty-facts/id1122926380?utm_source=opff&utf_medium=web",
#facebook_page_url => "https://www.facebook.com/openbeautyfacts?utm_source=opff&utf_medium=web",
#twitter_account => "OpenBeautyFacts",
+ default_preferences =>
+ '{ "nova" : "important", "labels_organic" : "important", "labels_fair_trade" : "important" }',
+ # favicon HTML and images generated with https://realfavicongenerator.net/ using the SVG icon
+ favicons => <
+
+
+
+
+
+
+
+
+HTML
+ ,
);
$options{export_limit} = 10000;
@@ -510,6 +525,20 @@ XML
last_image_t
);
+# Used to generate the list of possible product attributes, which is
+# used to display the possible choices for user preferences
+$options{attribute_groups} = [["labels", ["labels_organic", "labels_fair_trade"]],];
+
+# default preferences for attributes
+$options{attribute_default_preferences} = {
+ "labels_organic" => "important",
+ "labels_fair_trade" => "important",
+};
+
+use JSON::MaybeXS;
+$options{attribute_default_preferences_json}
+ = JSON->new->utf8->canonical->encode($options{attribute_default_preferences});
+
# for ingredients OCR, we use tesseract-ocr
# on debian, dictionaries are in /usr/share/tesseract-ocr/tessdata
# %tesseract_ocr_available_languages provides mapping between OFF 2 letter language codes
diff --git a/lib/ProductOpener/Display.pm b/lib/ProductOpener/Display.pm
index f763bd4aab648..5c728c65c9dba 100644
--- a/lib/ProductOpener/Display.pm
+++ b/lib/ProductOpener/Display.pm
@@ -961,11 +961,9 @@ CSS
$knowledge_panels_options_ref = {};
if (not feature_enabled("ecoscore")) {
- $attributes_options_ref->{skip_ecoscore} = 1;
$knowledge_panels_options_ref->{skip_ecoscore} = 1;
}
if (not feature_enabled("forest_footprint")) {
- $attributes_options_ref->{skip_forest_footprint} = 1;
$knowledge_panels_options_ref->{skip_forest_footprint} = 1;
}
@@ -1340,6 +1338,10 @@ sub display_text ($request_ref) {
}
my $file = "$BASE_DIRS{LANG}/$text_lc/texts/" . $texts{$textid}{$text_lc};
+ # Check if we have a flavor specific version
+ if (-e "$BASE_DIRS{LANG}/$flavor/$text_lc/texts/" . $texts{$textid}{$text_lc}) {
+ $file = "$BASE_DIRS{LANG}/$flavor/$text_lc/texts/" . $texts{$textid}{$text_lc};
+ }
display_text_content($request_ref, $textid, $text_lc, $file);
return;
@@ -4547,6 +4549,7 @@ sub display_search_results ($request_ref) {
$request_ref->{scripts} .= <
var page_type = "products";
+var default_preferences = $options{attribute_default_preferences_json};
var preferences_text = "$preferences_text";
var contributor_prefs = $contributor_prefs_json;
var products = [];
@@ -5705,6 +5708,7 @@ sub search_and_display_products ($request_ref, $query_ref, $sort_by, $limit, $pa
$request_ref->{scripts} .= <
var page_type = "products";
+var default_preferences = $options{attribute_default_preferences_json};
var preferences_text = "$preferences_text";
var contributor_prefs = $contributor_prefs_json;
var products = $products_json;
@@ -7956,6 +7960,10 @@ JS
= display_knowledge_panel($product_ref, $product_ref->{"knowledge_panels_" . $lc}, "environment_card");
$template_data_ref->{health_card_panel}
= display_knowledge_panel($product_ref, $product_ref->{"knowledge_panels_" . $lc}, "health_card");
+ if ($product_ref->{"knowledge_panels_" . $lc}{"secondhand_card"}) {
+ $template_data_ref->{secondhand_card_panel}
+ = display_knowledge_panel($product_ref, $product_ref->{"knowledge_panels_" . $lc}, "secondhand_card");
+ }
$template_data_ref->{report_problem_card_panel}
= display_knowledge_panel($product_ref, $product_ref->{"knowledge_panels_" . $lc}, "report_problem_card");
if ($product_ref->{"knowledge_panels_" . $lc}{"contribution_card"}) {
@@ -8569,6 +8577,7 @@ HTML
$request_ref->{scripts} .= <
var page_type = "product";
+var default_preferences = $options{attribute_default_preferences_json};
var preferences_text = "$preferences_text";
var product = $product_attribute_groups_json;
diff --git a/lib/ProductOpener/Index.pm b/lib/ProductOpener/Index.pm
index e09b2d3db666b..a85c88d7f85a3 100644
--- a/lib/ProductOpener/Index.pm
+++ b/lib/ProductOpener/Index.pm
@@ -80,37 +80,42 @@ if (not -e $lang_dir) {
) if $log->is_warn();
}
-if (opendir DH2, $lang_dir) {
-
- $log->info("Reading texts from $lang_dir") if $log->is_info();
-
- foreach my $langid (readdir(DH2)) {
- next if $langid eq '.';
- next if $langid eq '..';
- #$log->trace("reading texts", { lang => $langid }) if $log->is_trace();
- next if ((length($langid) ne 2) and not($langid eq 'other'));
-
- if (-e "$lang_dir/$langid/texts") {
- opendir DH, "$lang_dir/$langid/texts" or die "Couldn't open $lang_dir/$langid/texts: $!";
- foreach my $textid (readdir(DH)) {
- next if $textid eq '.';
- next if $textid eq '..';
- my $file = $textid;
- $textid =~ s/(\.foundation)?(\.$langid)?\.html//;
- defined $texts{$textid} or $texts{$textid} = {};
- # prefer the .foundation version
- if ((not defined $texts{$textid}{$langid}) or (length($file) > length($texts{$textid}{$langid}))) {
- $texts{$textid}{$langid} = $file;
+# Check both $lang_dir + flavor specific directory
+
+foreach my $dir ($lang_dir, "$lang_dir/$flavor") {
+
+ if (opendir DH2, $dir) {
+
+ $log->info("Reading texts from $lang_dir") if $log->is_info();
+
+ foreach my $langid (readdir(DH2)) {
+ next if $langid eq '.';
+ next if $langid eq '..';
+ #$log->trace("reading texts", { lang => $langid }) if $log->is_trace();
+ next if ((length($langid) ne 2) and not($langid eq 'other'));
+
+ if (-e "$dir/$langid/texts") {
+ opendir DH, "$dir/$langid/texts" or die "Couldn't open $dir/$langid/texts: $!";
+ foreach my $textid (readdir(DH)) {
+ next if $textid eq '.';
+ next if $textid eq '..';
+ my $file = $textid;
+ $textid =~ s/(\.foundation)?(\.$langid)?\.html//;
+ defined $texts{$textid} or $texts{$textid} = {};
+ # prefer the .foundation version
+ if ((not defined $texts{$textid}{$langid}) or (length($file) > length($texts{$textid}{$langid}))) {
+ $texts{$textid}{$langid} = $file;
+ }
+
+ #$log->trace("text loaded", { langid => $langid, textid => $textid }) if $log->is_trace();
}
-
- #$log->trace("text loaded", { langid => $langid, textid => $textid }) if $log->is_trace();
+ closedir(DH);
}
- closedir(DH);
}
+ closedir(DH2);
}
- closedir(DH2);
}
-else {
+if (scalar keys %texts == 0) {
$log->error("Texts could not be loaded.") if $log->is_error();
die("Texts could not be loaded from $BASE_DIRS{LANG} or $BASE_DIRS{LANG}-default");
}
diff --git a/lib/ProductOpener/KnowledgePanels.pm b/lib/ProductOpener/KnowledgePanels.pm
index 47abeb1557d88..5245cbbef3b9b 100644
--- a/lib/ProductOpener/KnowledgePanels.pm
+++ b/lib/ProductOpener/KnowledgePanels.pm
@@ -241,6 +241,12 @@ sub create_knowledge_panels ($product_ref, $target_lc, $target_cc, $options_ref,
$has_contribution_card = create_contribution_card_panel($product_ref, $target_lc, $target_cc, $options_ref);
}
+ my $has_secondhand_card;
+ if ($panel_is_requested->('secondhand_card')) {
+ $has_secondhand_card
+ = create_secondhand_card_panel($product_ref, $target_lc, $target_cc, $options_ref, $request_ref);
+ }
+
# Create the root panel that contains the panels we want to show directly on the product page
create_panel_from_json_template(
"root",
@@ -250,6 +256,7 @@ sub create_knowledge_panels ($product_ref, $target_lc, $target_cc, $options_ref,
has_report_problem_card => $has_report_problem_card,
has_contribution_card => $has_contribution_card,
has_environment_card => $has_environment_card,
+ has_secondhand_card => $has_secondhand_card,
},
$product_ref,
$target_lc,
@@ -632,7 +639,8 @@ sub create_ecoscore_panel ($product_ref, $target_lc, $target_cc, $options_ref, $
# }
# }
- create_panel_from_json_template("carbon_footprint", "api/knowledge-panels/environment/carbon_footprint.tt.json",
+ create_panel_from_json_template("carbon_footprint",
+ "api/knowledge-panels/environment/carbon_footprint_food.tt.json",
$panel_data_ref, $product_ref, $target_lc, $target_cc, $options_ref);
# Add panels for the different bonuses and maluses
@@ -765,6 +773,11 @@ sub create_environment_card_panel ($product_ref, $target_lc, $target_cc, $option
}
}
+ # Create panel for carbon footprint (non-food products, for food products, it is added by create_ecoscore_panel)
+ if ($options{product_type} ne "food") {
+ create_carbon_footprint_panel($product_ref, $target_lc, $target_cc, $options_ref);
+ }
+
# Create panel for packaging components, and packaging materials
create_panel_from_json_template("packaging_recycling",
"api/knowledge-panels/environment/packaging_recycling.tt.json",
@@ -780,22 +793,111 @@ sub create_environment_card_panel ($product_ref, $target_lc, $target_cc, $option
create_manufacturing_place_panel($product_ref, $target_lc, $target_cc, $options_ref);
# Origins of ingredients for the environment card, for food, pet food and beauty products
- if ( ($options{product_type} eq "food")
- or ($options{product_type} eq "pet_food")
- or ($options{product_type} eq "beauty"))
- {
+ if (feature_enabled("ingredients")) {
create_panel_from_json_template("origins_of_ingredients",
"api/knowledge-panels/environment/origins_of_ingredients.tt.json",
$panel_data_ref, $product_ref, $target_lc, $target_cc, $options_ref);
}
# Create the environment_card panel
- $panel_data_ref->{packaging_image} = data_to_display_image($product_ref, "packaging", $target_lc),
- create_panel_from_json_template("environment_card", "api/knowledge-panels/environment/environment_card.tt.json",
+ $panel_data_ref->{packaging_image} = data_to_display_image($product_ref, "packaging", $target_lc);
+ create_panel_from_json_template("environment_card", "api/knowledge-panels/environment/environment_card.tt.json",
+ $panel_data_ref, $product_ref, $target_lc, $target_cc, $options_ref);
+ return 1;
+}
+
+=head2 create_secondhand_card_panel ( $product_ref, $target_lc, $target_cc, $options_ref )
+
+Creates a knowledge panel card that contains all knowledge panels related to the circular economy:
+- sharing, buying, selling etc.
+
+Created for products in specific categories, for users in specific countries.
+
+=head3 Arguments
+
+=head4 product reference $product_ref
+
+Loaded from the MongoDB database, Storable files, or the OFF API.
+
+=head4 language code $target_lc
+
+Returned attributes contain both data and strings intended to be displayed to users.
+This parameter sets the desired language for the user facing strings.
+
+=head4 country code $target_cc
+
+Used to select secondhand options (e.g. classified ads sites) that are relevant for the user.
+
+=cut
+
+sub create_secondhand_card_panel ($product_ref, $target_lc, $target_cc, $options_ref, $request_ref) {
+
+ $log->debug("create secondhand card panel", {code => $product_ref->{code}}) if $log->is_debug();
+
+ my $panel_data_ref = {};
+
+ # Only available for the product_type "product"
+ if ($options{product_type} ne "product") {
+ return 0;
+ }
+
+ # Add the name of the most specific category (last in categories_hierarchy) to the panel data
+ my $category_id = $product_ref->{categories_hierarchy}[-1];
+ $panel_data_ref->{category_name} = display_taxonomy_tag_name($target_lc, "categories", $category_id);
+
+ # Create paneld for donations
+
+ create_panel_from_json_template("donated_products_fr_geev",
+ "api/knowledge-panels/secondhand/donated_products_fr_geev.tt.json",
+ $panel_data_ref, $product_ref, $target_lc, $target_cc, $options_ref);
+
+ create_panel_from_json_template("donated_products", "api/knowledge-panels/secondhand/donated_products.tt.json",
+ $panel_data_ref, $product_ref, $target_lc, $target_cc, $options_ref);
+
+ # Created panels for buying used products
+ create_panel_from_json_template("used_products_fr_backmarket",
+ "api/knowledge-panels/secondhand/used_products_fr_backmarket.tt.json",
+ $panel_data_ref, $product_ref, $target_lc, $target_cc, $options_ref);
+
+ create_panel_from_json_template("used_products", "api/knowledge-panels/secondhand/used_products.tt.json",
+ $panel_data_ref, $product_ref, $target_lc, $target_cc, $options_ref);
+
+ # Create the secondhand_card panel
+
+ create_panel_from_json_template("secondhand_card", "api/knowledge-panels/secondhand/secondhand_card.tt.json",
$panel_data_ref, $product_ref, $target_lc, $target_cc, $options_ref);
+
return 1;
}
+sub create_carbon_footprint_panel($product_ref, $target_lc, $target_cc, $options_ref) {
+
+ # Find the first category that has a carbon_impact_fr_impactco2:en: property
+ my ($value, $category_id)
+ = get_inherited_property_from_categories_tags($product_ref, "carbon_impact_fr_impactco2:en");
+
+ $log->debug("create carbon footprint panel",
+ {code => $product_ref->{code}, category_id => $category_id, value => $value})
+ if $log->is_debug();
+
+ if (defined $value) {
+
+ my $panel_data_ref = {
+ category_id => $category_id,
+ category_name => display_taxonomy_tag_name($target_lc, "categories", $category_id),
+ co2_kg_per_unit => $value,
+ unit_name => get_property_with_fallbacks("categories", $category_id, "unit_name:$target_lc"),
+ link => get_property("categories", $category_id, "carbon_impact_fr_impactco2_link:en"),
+ };
+
+ create_panel_from_json_template("carbon_footprint",
+ "api/knowledge-panels/environment/carbon_footprint_product.tt.json",
+ $panel_data_ref, $product_ref, $target_lc, $target_cc, $options_ref);
+ }
+
+ return;
+}
+
=head2 create_manufacturing_place_panel ( $product_ref, $target_lc, $target_cc, $options_ref )
Creates a knowledge panel when we know the location of the manufacturing place,
diff --git a/lib/ProductOpener/Paths.pm b/lib/ProductOpener/Paths.pm
index 8662de0fb6a82..dc1b4fd0da056 100644
--- a/lib/ProductOpener/Paths.pm
+++ b/lib/ProductOpener/Paths.pm
@@ -42,8 +42,8 @@ BEGIN {
@EXPORT_OK = qw(
%BASE_DIRS
- &get_path_for_taxonomy
- &get_file_for_taxonomy
+ &get_path_for_taxonomy_file
+ &get_files_for_taxonomy
&base_paths
&base_paths_loading_script
&check_missing_dirs
@@ -334,7 +334,7 @@ sub _source_dir() {
return $src_root;
}
-=head2 get_file_for_taxonomy( $tagtype )
+=head2 get_files_for_taxonomy( $tagtype )
Taxonomy .txt source files are stored in the /taxonomies directory.
@@ -349,15 +349,18 @@ e.g. OFF ingredients are in /taxonomies/food/ingredients.txt and OBF ingredients
=cut
-sub get_file_for_taxonomy ($tagtype, $product_type) {
+sub get_files_for_taxonomy ($tagtype, $product_type) {
- my $file = $tagtype . '.txt';
- # If the flavor has a specific product type, first check if we have a source file for this product type
+ my @files = ();
+ # Check if there is a common taxonomy file for this tag type
+ if (-e "$BASE_DIRS{TAXONOMIES_SRC}/$tagtype.txt") {
+ push @files, "$tagtype.txt";
+ }
+ # Check if there is product type specific taxonomy file for this tag type
if ((defined $product_type) and (-e "$BASE_DIRS{TAXONOMIES_SRC}/$product_type/$tagtype.txt")) {
- $file = "$product_type/$tagtype.txt";
+ push @files, "$product_type/$tagtype.txt";
}
-
- return $file;
+ return @files;
}
=head2 get_path_for_taxonomy( $tagtype, $product_type )
@@ -366,9 +369,8 @@ full path for taxonomy file
=cut
-sub get_path_for_taxonomy($tagtype, $product_type) {
+sub get_path_for_taxonomy_file($source_file) {
# The source file can be prefixed by the product type
- my $source_file = get_file_for_taxonomy($tagtype, $product_type);
return "$BASE_DIRS{TAXONOMIES_SRC}/$source_file";
}
diff --git a/lib/ProductOpener/Products.pm b/lib/ProductOpener/Products.pm
index 8af458c933e0c..1eefc59a31789 100644
--- a/lib/ProductOpener/Products.pm
+++ b/lib/ProductOpener/Products.pm
@@ -772,6 +772,7 @@ sub init_product ($userid, $orgid, $code, $countryid) {
created_t => time(),
creator => $creator,
rev => 0,
+ product_type => $options{product_type},
};
if (defined $server) {
@@ -889,7 +890,6 @@ sub retrieve_product ($product_id) {
my $product_ref = retrieve($full_product_path);
- # If the product is on another server, set the server field so that it will be saved in the other server if we save it
my $server = server_for_product_id($product_id);
if (not defined $product_ref) {
@@ -898,10 +898,9 @@ sub retrieve_product ($product_id) {
if $log->is_debug();
}
else {
- if (defined $server) {
- $product_ref->{server} = $server;
+ if ($product_ref->{deleted}) {
$log->debug(
- "retrieve_product - product on another server",
+ "retrieve_product - deleted product",
{
product_id => $product_id,
product_data_root => $product_data_root,
@@ -909,11 +908,15 @@ sub retrieve_product ($product_id) {
server => $server
}
) if $log->is_debug();
+ return;
}
- if ($product_ref->{deleted}) {
+ # If the product is on another server, set the server field so that it will be saved in the other server if we save it
+
+ if (defined $server) {
+ $product_ref->{server} = $server;
$log->debug(
- "retrieve_product - deleted product",
+ "retrieve_product - product on another server",
{
product_id => $product_id,
product_data_root => $product_data_root,
@@ -921,7 +924,10 @@ sub retrieve_product ($product_id) {
server => $server
}
) if $log->is_debug();
- return;
+ }
+ else {
+ # If the product was moved previously, it may have a server field, remove it
+ delete $product_ref->{server};
}
}
@@ -935,12 +941,6 @@ sub retrieve_product_or_deleted_product ($product_id, $deleted_ok = 1) {
my $product_ref = retrieve("$product_data_root/products/$path/product.sto");
- # If the product is on another server, set the server field so that it will be saved in the other server if we save it
- my $server = server_for_product_id($product_id);
- if ((defined $product_ref) and (defined $server)) {
- $product_ref->{server} = $server;
- }
-
if ( (defined $product_ref)
and ($product_ref->{deleted})
and (not $deleted_ok))
@@ -948,6 +948,18 @@ sub retrieve_product_or_deleted_product ($product_id, $deleted_ok = 1) {
return;
}
+ if (defined $product_ref) {
+ # If the product is on another server, set the server field so that it will be saved in the other server if we save it
+ my $server = server_for_product_id($product_id);
+ if (defined $server) {
+ $product_ref->{server} = $server;
+ }
+ else {
+ # If the product was moved previously, it may have a server field, remove it
+ delete $product_ref->{server};
+ }
+ }
+
return $product_ref;
}
@@ -962,14 +974,21 @@ sub retrieve_product_rev ($product_id, $rev) {
my $product_ref = retrieve("$product_data_root/products/$path/$rev.sto");
- # If the product is on another server, set the server field so that it will be saved in the other server if we save it
- my $server = server_for_product_id($product_id);
- if ((defined $product_ref) and (defined $server)) {
- $product_ref->{server} = $server;
- }
+ if (defined $product_ref) {
- if ((defined $product_ref) and ($product_ref->{deleted})) {
- return;
+ if ($product_ref->{deleted}) {
+ return;
+ }
+
+ # If the product is on another server, set the server field so that it will be saved in the other server if we save it
+ my $server = server_for_product_id($product_id);
+ if (defined $server) {
+ $product_ref->{server} = $server;
+ }
+ else {
+ # If the product was moved previously, it may have a server field, remove it
+ delete $product_ref->{server};
+ }
}
return $product_ref;
diff --git a/lib/ProductOpener/ProductsFeatures.pm b/lib/ProductOpener/ProductsFeatures.pm
index 4ef50234aa315..48c1eed92f3d8 100644
--- a/lib/ProductOpener/ProductsFeatures.pm
+++ b/lib/ProductOpener/ProductsFeatures.pm
@@ -74,13 +74,15 @@ my %product_type_features = (
additives => 1,
nova => 1,
nutrition => 1,
+ user_preferences => 1,
},
beauty => {
health_card => 1,
ingredients => 1,
+ user_preferences => 1,
},
- products => {
-
+ product => {
+ user_preferences => 1,
},
);
@@ -107,7 +109,13 @@ Currently not used, may be used later to determine features based on product fie
=cut
sub feature_enabled($feature, $product_ref = undef) {
- my $enabled = deep_get(\%product_type_features, $options{product_type}, $feature);
+ # If we have a product reference, and the product type is set, use it
+ # otherwise use the product type of the site instance (e.g. "Open Food Facts" -> "food")
+ my $product_type
+ = ((defined $product_ref) and (defined $product_ref->{product_type}))
+ ? $product_ref->{product_type}
+ : $options{product_type};
+ my $enabled = deep_get(\%product_type_features, $product_type, $feature);
$log->debug("feature_enabled", {feature => $feature, product_type => $options{product_type}, enabled => $enabled})
if $log->is_debug();
return $enabled;
diff --git a/lib/ProductOpener/Tags.pm b/lib/ProductOpener/Tags.pm
index 92d329c05e6cf..de77b18376f1e 100644
--- a/lib/ProductOpener/Tags.pm
+++ b/lib/ProductOpener/Tags.pm
@@ -176,7 +176,7 @@ use vars @EXPORT_OK;
use ProductOpener::Store qw/:all/;
use ProductOpener::Config qw/:all/;
-use ProductOpener::Paths qw/%BASE_DIRS ensure_dir_created_or_die get_file_for_taxonomy get_path_for_taxonomy/;
+use ProductOpener::Paths qw/%BASE_DIRS ensure_dir_created_or_die get_files_for_taxonomy get_path_for_taxonomy_file/;
use ProductOpener::Lang qw/$lc %Lang %tag_type_singular lang/;
use ProductOpener::Text qw/normalize_percentages regexp_escape/;
use ProductOpener::PackagerCodes qw/localize_packager_code normalize_packager_codes/;
@@ -1072,6 +1072,7 @@ sub get_lc_tagid ($synonyms_ref, $lc, $tagtype, $tag, $warning) {
sub get_file_from_cache ($source, $target) {
my $cache_root = "$BASE_DIRS{CACHE_BUILD}/taxonomies";
+ (-e $cache_root) or mkdir($cache_root, 0755);
my $local_cache_source = "$cache_root/$source";
# first, try to get it localy
@@ -1113,7 +1114,7 @@ sub get_from_cache ($tagtype, @files) {
foreach my $source_file (@files) {
# The source file can be prefixed by the product type
- my $source_path = get_path_for_taxonomy($source_file, $options{product_type});
+ my $source_path = get_path_for_taxonomy_file($source_file);
open(my $IN, "<", $source_path)
or die("Cannot open $source_path (tagtype: $tagtype - product_type: $options{product_type}): $!\n");
@@ -1238,16 +1239,20 @@ sub build_tags_taxonomy ($tagtype, $publish) {
my $result_dir = "$BASE_DIRS{CACHE_BUILD}/taxonomies-result/";
ensure_dir_created_or_die("$result_dir");
- my @files = ($tagtype);
+ # Some taxonomy tag types include other tag types (e.g. origins includes countries)
+ my @tagtypes = ($tagtype);
# For the origins taxonomy, include the countries taxonomy
if ($tagtype eq "origins") {
- @files = ("countries", "origins");
+ @tagtypes = ("countries", "origins");
}
# For the Open Food Facts ingredients taxonomy, concatenate additives, minerals, vitamins, nucleotides and other nutritional substances taxonomies
- elsif (($tagtype eq "ingredients") and (defined $options{product_type}) and ($options{product_type} eq "food")) {
- @files = (
+ elsif ( ($tagtype eq "ingredients")
+ and (defined $options{product_type})
+ and (($options{product_type} eq "food") or ($options{product_type} eq "petfood")))
+ {
+ @tagtypes = (
"additives_classes", "additives", "minerals", "vitamins",
"nucleotides", "other_nutritional_substances", "ingredients"
);
@@ -1255,14 +1260,27 @@ sub build_tags_taxonomy ($tagtype, $publish) {
# Packaging
elsif (($tagtype eq "packaging")) {
- @files = ("packaging_materials", "packaging_shapes", "packaging_recycling", "preservation");
+ @tagtypes = ("packaging_materials", "packaging_shapes", "packaging_recycling", "preservation");
}
# Traces - just a copy of allergens
elsif ($tagtype eq "traces") {
- @files = ("allergens");
+ @tagtypes = ("allergens");
}
+ # List the individual taxonomy source files for all included tag types
+ # Each tag type can have a common and/or a product type specific source file.
+
+ my @files = ();
+ foreach my $tagtype (@tagtypes) {
+ my @tagtype_files = get_files_for_taxonomy($tagtype, $options{product_type});
+ if (scalar @tagtype_files == 0) {
+ die("No taxonomy file(s) found for $tagtype\n");
+ }
+ push @files, @tagtype_files;
+ }
+
+ # Check if we already have a cached version of the taxonomy
my $cache_prefix = get_from_cache($tagtype, @files);
if (!$cache_prefix) {
return;
@@ -1271,20 +1289,22 @@ sub build_tags_taxonomy ($tagtype, $publish) {
print("building taxonomy for $tagtype - publish: $publish\n");
# Concatenate taxonomy files if needed
- my $file = get_file_for_taxonomy($tagtype, $options{product_type});
- my $file_path = get_path_for_taxonomy($tagtype, $options{product_type});
- if ((scalar @files) > 1) {
- $file = "$tagtype.all.txt";
- $file_path = "$result_dir/$file";
+ my $file_path;
+ if ((scalar @files) == 1) {
+ # Only 1 file
+ $file_path = get_path_for_taxonomy_file($files[0]);
+ }
+ else {
+ # Multiple files
+ $file_path = "$result_dir/$tagtype.all.txt";
open(my $OUT, ">:encoding(UTF-8)", $file_path)
or die("Cannot write $file_path : $!\n");
- foreach my $taxonomy (@files) {
- my $taxonomy_file = get_file_for_taxonomy($taxonomy, $options{product_type});
- my $taxonomy_path = get_path_for_taxonomy($taxonomy, $options{product_type});
+ foreach my $taxonomy_file (@files) {
+ my $taxonomy_path = get_path_for_taxonomy_file($taxonomy_file);
open(my $IN, "<:encoding(UTF-8)", $taxonomy_path)
- or die("Missing $taxonomy_path\n");
+ or die("Cannot open $taxonomy_path: $!\n");
print $OUT "# $taxonomy_file\n\n";
@@ -3089,64 +3109,10 @@ sub get_taxonomy_tag_and_link_for_lang ($target_lc, $tagtype, $tagid) {
my $taxonomy = $taxonomy_fields{$tagtype};
- my $tag_lc;
-
- if ($tagid =~ /^(\w\w):/) {
- $tag_lc = $1;
- }
-
- my $display = '';
- my $display_lc = "en"; # Default to English
- my $exists_in_taxonomy = 0;
-
- if ( (defined $translations_to{$taxonomy})
- and (defined $translations_to{$taxonomy}{$tagid})
- and (defined $translations_to{$taxonomy}{$tagid}{$target_lc}))
- {
- # we have a translation for the target language
- # print STDERR "display_taxonomy_tag - translation for the target language - translations_to{$taxonomy}{$tagid}{$target_lc} : $translations_to{$taxonomy}{$tagid}{$target_lc}\n";
- $display = $translations_to{$taxonomy}{$tagid}{$target_lc};
- $display_lc = $target_lc;
- $exists_in_taxonomy = 1;
- }
- else {
- # use tag language
- if ( (defined $translations_to{$taxonomy})
- and (defined $translations_to{$taxonomy}{$tagid})
- and (defined $tag_lc)
- and (defined $translations_to{$taxonomy}{$tagid}{$tag_lc}))
- {
- # we have a translation for the tag language
- # print STDERR "display_taxonomy_tag - translation for the tag language - translations_to{$taxonomy}{$tagid}{$tag_lc} : $translations_to{$taxonomy}{$tagid}{$tag_lc}\n";
-
- $display = "$tag_lc:" . $translations_to{$taxonomy}{$tagid}{$tag_lc};
-
- $exists_in_taxonomy = 1;
- }
- else {
- $display = $tagid;
- if (defined $tag_lc) {
- $display_lc = $tag_lc;
- }
-
- if ($target_lc eq $tag_lc) {
- $display =~ s/^(\w\w)://;
- }
- # print STDERR "display_taxonomy_tag - no translation available for $taxonomy $tagid in target language $lc or tag language $tag_lc - result: $display\n";
- }
- }
-
- # for additives, add the first synonym
- if ($taxonomy =~ /^additives(|_prev|_next|_debug)$/) {
- $tagid =~ s/.*://;
- if ( (defined $synonyms_for{$taxonomy}{$target_lc})
- and (defined $synonyms_for{$taxonomy}{$target_lc}{$tagid})
- and (defined $synonyms_for{$taxonomy}{$target_lc}{$tagid}[1]))
- {
- $display .= " - " . ucfirst($synonyms_for{$taxonomy}{$target_lc}{$tagid}[1]);
- }
- }
+ my $exists_in_taxonomy = exists_taxonomy_tag($tagtype, $tagid) || 0;
+ my $display = display_taxonomy_tag($target_lc, $tagtype, $tagid);
+ my $display_lc = $target_lc;
my $display_lc_prefix = "";
my $display_tag = $display;
@@ -4606,6 +4572,7 @@ sub init_tags_texts {
return if (%tags_texts);
$log->info("loading tags texts") if $log->is_info();
+
if (opendir DH2, $lang_dir) {
foreach my $langid (readdir(DH2)) {
next if $langid eq '.';
diff --git a/po/common/common.pot b/po/common/common.pot
index da7183b0d6396..217c0a1c1cfb7 100644
--- a/po/common/common.pot
+++ b/po/common/common.pot
@@ -5005,21 +5005,21 @@ msgctxt "ecoscore_information"
msgid "Information about the Eco-score"
msgstr "Information about the Eco-score"
-msgctxt "preferences_edit_your_food_preferences"
-msgid "Edit your food preferences"
-msgstr "Edit your food preferences"
-
-msgctxt "preferences_your_preferences"
-msgid "Your food preferences"
-msgstr "Your food preferences"
-
msgctxt "preferences_currently_selected_preferences"
msgid "Currently selected preferences"
msgstr "Currently selected preferences"
msgctxt "preferences_locally_saved"
-msgid "Your food preferences are kept in your browser and never sent to Open Food Facts or anyone else."
-msgstr "Your food preferences are kept in your browser and never sent to Open Food Facts or anyone else."
+msgid "Your preferences are kept in your browser and never sent to Open Food Facts or anyone else."
+msgstr "Your preferences are kept in your browser and never sent to Open Food Facts or anyone else."
+
+msgctxt "preferences_edit_your_preferences"
+msgid "Edit your preferences"
+msgstr "Edit your preferences"
+
+msgctxt "preferences_your_preferences"
+msgid "Your preferences"
+msgstr "Your preferences"
# used in phrases like "salt in unknown quantity"
msgctxt "unknown_quantity"
@@ -5850,8 +5850,21 @@ msgstr "Carbon footprint"
# variable names between { } must not be translated
msgctxt "f_carbon_footprint_per_100g_of_product"
-msgid "{grams} g CO² per 100g of product"
-msgstr "{grams} g CO² per 100g of product"
+msgid "{grams} g CO₂e per 100g of product"
+msgstr "{grams} g CO₂e per 100g of product"
+
+# variable names between { } must not be translated
+msgctxt "f_carbon_footprint_per_unit"
+msgid "{kilograms} kg CO₂e per unit"
+msgstr "{kilograms} kg CO₂e per unit"
+
+msgctxt "average_for_the_category"
+msgid "average for the category"
+msgstr "average for the category"
+
+msgctxt "data_source_and_detailed_carbon_impact"
+msgid "Data source and detailed carbon impact"
+msgstr "Data source and detailed carbon impact"
# variable names between { } must not be translated
msgctxt "f_equal_to_driving_km_in_a_petrol_car"
@@ -7119,3 +7132,77 @@ msgstr "Organization list"
msgctxt "open_org"
msgid "Open org"
msgstr "Open org"
+
+msgctxt "secondhand"
+msgid "Secondhand"
+msgstr "Secondhand"
+
+msgctxt "donated_products_title"
+msgid "Donations"
+msgstr "Donations"
+
+msgctxt "donated_products_subtitle"
+msgid "Give this product, or search for a similar donated product"
+msgstr "Give this product, or search for a similar donated product"
+
+msgctxt "used_products_title"
+msgid "Used products"
+msgstr "Used products"
+
+msgctxt "used_products_subtitle"
+msgid "Buy this product used, or search for a similar used product"
+msgstr "Buy this product used, or search for a similar used product"
+
+msgctxt "attribute_repairability_index_france_name"
+msgid "Repairability index"
+msgstr "Repairability index"
+
+msgctxt "attribute_repairability_index_france_setting_name"
+msgid "Good repairability"
+msgstr "Good repairability"
+
+msgctxt "attribute_repairability_index_france_setting_note"
+msgid "Mandatory rating in France for certain products since 2021"
+msgstr "Mandatory rating in France for certain products since 2021"
+
+# keep %s, it will be replaced by the letter A, B, C, D or E
+msgctxt "attribute_repairability_index_france_grade_title"
+msgid "Repairability index %s"
+msgstr "Repairability index %s"
+
+msgctxt "attribute_repairability_index_france_unknown_title"
+msgid "Repairability index unknown"
+msgstr "Repairability index unknown"
+
+msgctxt "attribute_repairability_index_france_not_applicable_title"
+msgid "Repairability index not-applicable"
+msgstr "Repairability index not-applicable"
+
+msgctxt "attribute_repairability_index_france_not_applicable_description_short"
+msgid "Not-applicable for the category"
+msgstr "Not-applicable for the category"
+
+# variable names between { } must not be translated
+msgctxt "f_attribute_repairability_index_france_not_applicable_description"
+msgid "Applicable only for categories: {categories}"
+msgstr "Applicable only for categories: {categories}"
+
+msgctxt "attribute_repairability_index_france_very_good_description_short"
+msgid "Very good repairability"
+msgstr "Very good repairability"
+
+msgctxt "attribute_repairability_index_france_good_description_short"
+msgid "Good repairability"
+msgstr "Good repairability"
+
+msgctxt "attribute_repairability_index_france_average_description_short"
+msgid "Average repairability"
+msgstr "Average repairability"
+
+msgctxt "attribute_repairability_index_france_poor_description_short"
+msgid "Poor repairability"
+msgstr "Poor repairability"
+
+msgctxt "attribute_repairability_index_france_bad_description_short"
+msgid "Bad repairability"
+msgstr "Bad repairability"
diff --git a/po/common/en.po b/po/common/en.po
index ff904a0d64c4f..170b7486e5ab2 100644
--- a/po/common/en.po
+++ b/po/common/en.po
@@ -5029,21 +5029,21 @@ msgctxt "ecoscore_information"
msgid "Information about the Eco-score"
msgstr "Information about the Eco-score"
-msgctxt "preferences_edit_your_food_preferences"
-msgid "Edit your food preferences"
-msgstr "Edit your food preferences"
-
-msgctxt "preferences_your_preferences"
-msgid "Your food preferences"
-msgstr "Your food preferences"
-
msgctxt "preferences_currently_selected_preferences"
msgid "Currently selected preferences"
msgstr "Currently selected preferences"
msgctxt "preferences_locally_saved"
-msgid "Your food preferences are kept in your browser and never sent to Open Food Facts or anyone else."
-msgstr "Your food preferences are kept in your browser and never sent to Open Food Facts or anyone else."
+msgid "Your preferences are kept in your browser and never sent to Open Food Facts or anyone else."
+msgstr "Your preferences are kept in your browser and never sent to Open Food Facts or anyone else."
+
+msgctxt "preferences_edit_your_preferences"
+msgid "Edit your preferences"
+msgstr "Edit your preferences"
+
+msgctxt "preferences_your_preferences"
+msgid "Your preferences"
+msgstr "Your preferences"
# used in phrases like "salt in unknown quantity"
msgctxt "unknown_quantity"
@@ -5874,8 +5874,21 @@ msgstr "Carbon footprint"
# variable names between { } must not be translated
msgctxt "f_carbon_footprint_per_100g_of_product"
-msgid "{grams} g CO² per 100g of product"
-msgstr "{grams} g CO² per 100g of product"
+msgid "{grams} g CO₂e per 100g of product"
+msgstr "{grams} g CO₂e per 100g of product"
+
+# variable names between { } must not be translated
+msgctxt "f_carbon_footprint_per_unit"
+msgid "{kilograms} kg CO₂e per unit"
+msgstr "{kilograms} kg CO₂e per unit"
+
+msgctxt "average_for_the_category"
+msgid "average for the category"
+msgstr "average for the category"
+
+msgctxt "data_source_and_detailed_carbon_impact"
+msgid "Data source and detailed carbon impact"
+msgstr "Data source and detailed carbon impact"
# variable names between { } must not be translated
msgctxt "f_equal_to_driving_km_in_a_petrol_car"
@@ -7105,3 +7118,81 @@ msgstr "Organization list"
msgctxt "open_org"
msgid "Open org"
msgstr "Open org"
+
+msgctxt "secondhand"
+msgid "Secondhand"
+msgstr "Secondhand"
+
+msgctxt "donated_products_title"
+msgid "Donations"
+msgstr "Donations"
+
+msgctxt "donated_products_subtitle"
+msgid "Give this product, or search for a similar donated product"
+msgstr "Give this product, or search for a similar donated product"
+
+msgctxt "used_products_title"
+msgid "Used products"
+msgstr "Used products"
+
+msgctxt "used_products_subtitle"
+msgid "Buy this product used, or search for a similar used product"
+msgstr "Buy this product used, or search for a similar used product"
+
+msgctxt "attribute_repairability_index_france_name"
+msgid "Repairability index"
+msgstr "Repairability index"
+
+msgctxt "attribute_repairability_index_france_setting_name"
+msgid "Good repairability"
+msgstr "Good repairability"
+
+msgctxt "attribute_repairability_index_france_setting_note"
+msgid "Mandatory rating in France for certain products since 2021"
+msgstr "Mandatory rating in France for certain products since 2021"
+
+# keep %s, it will be replaced by the letter A, B, C, D or E
+msgctxt "attribute_repairability_index_france_grade_title"
+msgid "Repairability index %s"
+msgstr "Repairability index %s"
+
+msgctxt "attribute_repairability_index_france_unknown_title"
+msgid "Repairability index unknown"
+msgstr "Repairability index unknown"
+
+msgctxt "attribute_repairability_index_france_not_applicable_title"
+msgid "Repairability index not-applicable"
+msgstr "Repairability index not-applicable"
+
+msgctxt "attribute_repairability_index_france_not_applicable_description_short"
+msgid "Not-applicable for the category"
+msgstr "Not-applicable for the category"
+
+# variable names between { } must not be translated
+msgctxt "f_attribute_repairability_index_france_not_applicable_description"
+msgid "Applicable only for categories: {categories}"
+msgstr "Applicable only for categories: {categories}"
+
+msgctxt "attribute_repairability_index_france_very_good_description_short"
+msgid "Very good repairability"
+msgstr "Very good repairability"
+
+msgctxt "attribute_repairability_index_france_good_description_short"
+msgid "Good repairability"
+msgstr "Good repairability"
+
+msgctxt "attribute_repairability_index_france_average_description_short"
+msgid "Average repairability"
+msgstr "Average repairability"
+
+msgctxt "attribute_repairability_index_france_poor_description_short"
+msgid "Poor repairability"
+msgstr "Poor repairability"
+
+msgctxt "attribute_repairability_index_france_bad_description_short"
+msgid "Bad repairability"
+msgstr "Bad repairability"
+
+msgctxt "concerned_categories"
+msgid "Bad repairability"
+msgstr "Bad repairability"
diff --git a/po/common/fr.po b/po/common/fr.po
index 5263a15b4ab0f..3121f8de72bf8 100644
--- a/po/common/fr.po
+++ b/po/common/fr.po
@@ -4957,21 +4957,21 @@ msgctxt "ecoscore_information"
msgid "Information about the Eco-score"
msgstr "Information sur l'Eco-score"
-msgctxt "preferences_edit_your_food_preferences"
-msgid "Edit your food preferences"
-msgstr "Modifier vos préférences alimentaires"
-
-msgctxt "preferences_your_preferences"
-msgid "Your food preferences"
-msgstr "Vos préférences alimentaires"
-
msgctxt "preferences_currently_selected_preferences"
msgid "Currently selected preferences"
msgstr "Préférences actuellement sélectionnées"
msgctxt "preferences_locally_saved"
-msgid "Your food preferences are kept in your browser and never sent to Open Food Facts or anyone else."
-msgstr "Vos préférences alimentaires sont conservées dans votre navigateur et ne sont jamais envoyées à Open Food Facts ou à quiconque."
+msgid "Your preferences are kept in your browser and never sent to Open Food Facts or anyone else."
+msgstr "Vos préférences sont conservées dans votre navigateur et ne sont jamais envoyées à Open Food Facts ou à quiconque."
+
+msgctxt "preferences_edit_your_preferences"
+msgid "Edit your preferences"
+msgstr "Modifiez vos préférences"
+
+msgctxt "preferences_your_preferences"
+msgid "Your preferences"
+msgstr "Vos préférences"
# used in phrases like "salt in unknown quantity"
msgctxt "unknown_quantity"
@@ -5802,8 +5802,21 @@ msgstr "Empreinte carbone"
# variable names between { } must not be translated
msgctxt "f_carbon_footprint_per_100g_of_product"
-msgid "{grams} g CO² per 100g of product"
-msgstr "{grams} g de CO² pour 100g de produit"
+msgid "{grams} g CO₂e per 100g of product"
+msgstr "{grams} g CO₂e pour 100g de produit"
+
+# variable names between { } must not be translated
+msgctxt "f_carbon_footprint_per_unit"
+msgid "{kilograms} kg CO₂e per unit"
+msgstr "{kilograms} kg CO₂e par unité"
+
+msgctxt "average_for_the_category"
+msgid "average for the category"
+msgstr "moyenne pour la catégorie"
+
+msgctxt "data_source_and_detailed_carbon_impact"
+msgid "Data source and detailed carbon impact"
+msgstr "Source des données et détail de l'impact carbone"
# variable names between { } must not be translated
msgctxt "f_equal_to_driving_km_in_a_petrol_car"
@@ -6962,4 +6975,79 @@ msgstr "Liste des organisations"
msgctxt "open_org"
msgid "Open org"
-msgstr "Voir l'org"
+msgstr "Voir l'org."
+
+msgctxt "secondhand"
+msgid "Secondhand"
+msgstr "Deuxième main"
+
+msgctxt "donated_products_title"
+msgid "Donations"
+msgstr "Dons"
+
+msgctxt "donated_products_subtitle"
+msgid "Give this product, or search for a similar donated product"
+msgstr "Donnez ce produit, ou recherchez un produit similaire donné"
+
+msgctxt "used_products_title"
+msgid "Used products"
+msgstr "Produits d'occasion"
+
+msgctxt "used_products_subtitle"
+msgid "Buy this product used, or search for a similar used product"
+msgstr "Achetez ce produit d'occasion, ou recherchez un produit d'occasion similaire"
+
+msgctxt "attribute_repairability_index_france_name"
+msgid "Repairability index"
+msgstr "Indice de réparabilité"
+
+msgctxt "attribute_repairability_index_france_setting_name"
+msgid "Good repairability"
+msgstr "Bonne réparabilité"
+
+msgctxt "attribute_repairability_index_france_setting_note"
+msgid "Mandatory rating in France for certain products since 2021"
+msgstr "Note sur 10 obligatoire en France pour certains produits depuis 2021"
+
+# keep %s, it will be replaced by the letter A, B, C, D or E
+msgctxt "attribute_repairability_index_france_grade_title"
+msgid "Repairability index %s"
+msgstr "Indice de réparabilité %s"
+
+msgctxt "attribute_repairability_index_france_unknown_title"
+msgid "repairability index unknown"
+msgstr "Indice de réparabilité inconnu"
+
+msgctxt "attribute_repairability_index_france_not_applicable_title"
+msgid "Repairability index not-applicable"
+msgstr "Indice de réparabilité non applicable"
+
+msgctxt "attribute_repairability_index_france_not_applicable_description_short"
+msgid "Not-applicable for the category"
+msgstr "Non applicable pour la catégorie"
+
+# variable names between { } must not be translated
+msgctxt "f_attribute_repairability_index_france_not_applicable_description"
+msgid "Applicable only for categories: {categories}"
+msgstr "Applicable seulement aux catégories : {categories}"
+
+msgctxt "attribute_repairability_index_france_very_good_description_short"
+msgid "Very good repairability"
+msgstr "Très bonne réparabilité"
+
+msgctxt "attribute_repairability_index_france_good_description_short"
+msgid "Good repairability"
+msgstr "Bonne réparabilité"
+
+msgctxt "attribute_repairability_index_france_average_description_short"
+msgid "Average repairability"
+msgstr "Moyenne réparabilité"
+
+msgctxt "attribute_repairability_index_france_poor_description_short"
+msgid "Poor repairability"
+msgstr "Mauvaise réparabilité"
+
+msgctxt "attribute_repairability_index_france_bad_description_short"
+msgid "Bad repairability"
+msgstr "Très mauvaise réparabilité"
+
diff --git a/scripts/migrations/2024_09_detect_duplicate_products.pl b/scripts/migrations/2024_09_detect_duplicate_products.pl
new file mode 100755
index 0000000000000..714fa96b1292a
--- /dev/null
+++ b/scripts/migrations/2024_09_detect_duplicate_products.pl
@@ -0,0 +1,52 @@
+#!/usr/bin/perl -w
+
+# This file is part of Product Opener.
+#
+# Product Opener
+# Copyright (C) 2011-2023 Association Open Food Facts
+# Contact: contact@openfoodfacts.org
+# Address: 21 rue des Iles, 94100 Saint-Maur des Fossés, France
+#
+# Product Opener is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+use Modern::Perl '2017';
+use utf8;
+
+use ProductOpener::Config qw/:all/;
+use ProductOpener::Paths qw/%BASE_DIRS/;
+use ProductOpener::Store qw/retrieve store/;
+use ProductOpener::Data qw/get_products_collection/;
+
+use Log::Any::Adapter 'TAP';
+
+my $socket_timeout_ms = 2 * 60000; # 2 mins, instead of 30s default, to not die as easily if mongodb is busy.
+
+my %flavors = ();
+
+foreach my $flavor ("off", "obf") {
+ my $products_collection = get_products_collection({database => $flavor, timeout => $socket_timeout_ms});
+
+ my $cursor = $products_collection->query({})->fields({_id => 1, code => 1, owner => 1});
+ $cursor->immortal(1);
+
+ while (my $product_ref = $cursor->next) {
+ $flavors{all}{$product_ref->{code}}++;
+ $flavors{$flavor}{$product_ref->{code}}++;
+ }
+}
+
+foreach my $flavor (keys %flavors) {
+ print "Flavor $flavor\t" . scalar(keys %{$flavors{$flavor}}) . " products\n";
+}
+
diff --git a/scripts/update_all_products.pl b/scripts/update_all_products.pl
index fc25fe850f3a1..56af909dd8696 100755
--- a/scripts/update_all_products.pl
+++ b/scripts/update_all_products.pl
@@ -151,6 +151,7 @@
my $obsolete = 0;
my $fix_obsolete;
my $fix_last_modified_t; # Will set the update key and ensure last_updated_t is initialised
+my $add_product_type = ''; # Add product type to products that don't have it, based on off/opf/obf/opff flavor
my $query_params_ref = {}; # filters for mongodb query
@@ -212,6 +213,7 @@
"obsolete" => \$obsolete,
"fix-obsolete" => \$fix_obsolete,
"fix-last-modified-t" => \$fix_last_modified_t,
+ "add-product-type" => \$add_product_type,
) or die("Error in command line arguments:\n\n$usage");
use Data::Dumper;
@@ -289,6 +291,7 @@
and (not $assign_ciqual_codes)
and (not $fix_obsolete)
and (not $fix_last_modified_t)
+ and (not $add_product_type)
and (not $analyze_and_enrich_product_data))
{
die("Missing fields to update or --count option:\n$usage");
@@ -1365,6 +1368,15 @@
}
}
+ # Add product type
+ if (($add_product_type) and (not defined $product_ref->{product_type})) {
+ $product_ref->{product_type} = $options{product_type};
+ # Silent update: we also change the original_product
+ # in order not to push the product to Redis
+ $original_product->{product_type} = $product_ref->{product_type};
+ # $product_values_changed = 1;
+ }
+
if ($assign_ciqual_codes) {
assign_ciqual_codes($product_ref);
}
diff --git a/stop_words.txt b/stop_words.txt
index b505e2dedb4ae..08ca8af7ed42c 100644
--- a/stop_words.txt
+++ b/stop_words.txt
@@ -221,6 +221,7 @@ Prunus
publique
Pulpe
reimplemented
+repairability
rescale
Robotoff
RTFSG
diff --git a/taxonomies/petfood/categories.txt b/taxonomies/petfood/categories.txt
index c0b19a769bb09..9c336359327bd 100755
--- a/taxonomies/petfood/categories.txt
+++ b/taxonomies/petfood/categories.txt
@@ -1,142 +1,140 @@
-en:Dry food
-nl:Droog voedsel
-nl_be:Droog voedsel
-ru:Сухой корм
-
-en:Wet food
-nl:Nat voedsel
-nl_be:Nat voedsel
-
-en:Dog and cat food
-de:Hunde- und Katzenfutter
-fr:Nourriture pour chiens et chats
-nl:Honden en kattenvoedsel
-ru:Корм для собак и кошек
-
- "book". Used in particular for carbon footprint equivalent: 1 smartphone = 85.9 kg CO2e
+#
+# - carbon_impact_fr_impactco2:en: co2 equivalent in kg per unit (1 product) from https://impactco2.fr/
+# - carbon_impact_fr_impactco2_link:en: URL for the category on https://impactco2.fr/
+
+en: Electronic products, electronics
+fr: Appareils électroniques
+secondhand_used:en: backmarket
+
+< en: electronic products
+en: Digital tablets
+fr: Tablettes numériques, tablette numérique
+carbon_impact_fr_impactco2:en: 61.9
+carbon_impact_fr_impactco2_link:en: https://impactco2.fr/outils/numerique/tabletteclassique
+unit_name:en: digital tablet
+unit_name:fr: tablette numérique
+
+< en: electronic products
+en: mobile phones, cell phones
+fr: téléphones portables, téléphones mobiles
+
+< en: mobile phones
+en: Smartphones
+xx: Smartphones
+carbon_impact_fr_impactco2:en: 85.9
+carbon_impact_fr_impactco2_link:en: https://impactco2.fr/outils/numerique/smartphone
+unit_name:xx: smartphone
+unit_name:en: smartphone
+
+< en: Smartphones
+en: Android smartphones
+fr: Smartphones Android
+
+< en: Smartphones
+en: iPhone smartphones
+xx: iPhone, iPhones
+fr: Smartphones iPhone
+
+en: Clothes
+fr: Vêtements
+
+< en: Clothes
+en: T-shirts
+xx: T-shirts, Tshirts, teeshirts, teeshirt
+carbon_impact_fr_impactco2:en: 6.43
+carbon_impact_fr_impactco2_link:en: https://impactco2.fr/outils/habillement/tshirtencoton
+unit_name:xx: t-shirt
+unit_name:en: t-shirt
+
+en: Books
+fr: Livres
+
+en: Papers
+fr: Papiers
+
+< en: Papers
+en: Origami papers, origami craft papers
+
+en: Toilet papers, toilet tissues, toilet rolls
+fr: Papiers toilette, papier toilette, papier hygiénique, papiers hygiéniques
+
+en: Cigarettes
+fr: Cigarettes
+
+en: Laundry detergents
+fr: Lessives
+
diff --git a/taxonomies/product/labels.txt b/taxonomies/product/labels.txt
new file mode 100644
index 0000000000000..b025785781154
--- /dev/null
+++ b/taxonomies/product/labels.txt
@@ -0,0 +1,410 @@
+# Labels specific for Open Product Facts (generic products: not food, pet food or beauty)
+
+# French reparability index
+
+# Labels specific for Open Product Facts (generic products: not food, pet food or beauty)
+
+# French reparability index
+
+en: Repairability index 0 - France
+fr: Indice de réparabilité 0
+repairability_index_france_value:en: 0
+
+en: Repairability index 0.1 - France
+fr: Indice de réparabilité 0.1
+repairability_index_france_value:en: 0.1
+
+en: Repairability index 0.2 - France
+fr: Indice de réparabilité 0.2
+repairability_index_france_value:en: 0.2
+
+en: Repairability index 0.3 - France
+fr: Indice de réparabilité 0.3
+repairability_index_france_value:en: 0.3
+
+en: Repairability index 0.4 - France
+fr: Indice de réparabilité 0.4
+repairability_index_france_value:en: 0.4
+
+en: Repairability index 0.5 - France
+fr: Indice de réparabilité 0.5
+repairability_index_france_value:en: 0.5
+
+en: Repairability index 0.6 - France
+fr: Indice de réparabilité 0.6
+repairability_index_france_value:en: 0.6
+
+en: Repairability index 0.7 - France
+fr: Indice de réparabilité 0.7
+repairability_index_france_value:en: 0.7
+
+en: Repairability index 0.8 - France
+fr: Indice de réparabilité 0.8
+repairability_index_france_value:en: 0.8
+
+en: Repairability index 0.9 - France
+fr: Indice de réparabilité 0.9
+repairability_index_france_value:en: 0.9
+
+en: Repairability index 1.0 - France
+fr: Indice de réparabilité 1.0
+repairability_index_france_value:en: 1.0
+
+en: Repairability index 1.1 - France
+fr: Indice de réparabilité 1.1
+repairability_index_france_value:en: 1.1
+
+en: Repairability index 1.2 - France
+fr: Indice de réparabilité 1.2
+repairability_index_france_value:en: 1.2
+
+en: Repairability index 1.3 - France
+fr: Indice de réparabilité 1.3
+repairability_index_france_value:en: 1.3
+
+en: Repairability index 1.4 - France
+fr: Indice de réparabilité 1.4
+repairability_index_france_value:en: 1.4
+
+en: Repairability index 1.5 - France
+fr: Indice de réparabilité 1.5
+repairability_index_france_value:en: 1.5
+
+en: Repairability index 1.6 - France
+fr: Indice de réparabilité 1.6
+repairability_index_france_value:en: 1.6
+
+en: Repairability index 1.7 - France
+fr: Indice de réparabilité 1.7
+repairability_index_france_value:en: 1.7
+
+en: Repairability index 1.8 - France
+fr: Indice de réparabilité 1.8
+repairability_index_france_value:en: 1.8
+
+en: Repairability index 1.9 - France
+fr: Indice de réparabilité 1.9
+repairability_index_france_value:en: 1.9
+
+en: Repairability index 2.0 - France
+fr: Indice de réparabilité 2.0
+repairability_index_france_value:en: 2.0
+
+en: Repairability index 2.1 - France
+fr: Indice de réparabilité 2.1
+repairability_index_france_value:en: 2.1
+
+en: Repairability index 2.2 - France
+fr: Indice de réparabilité 2.2
+repairability_index_france_value:en: 2.2
+
+en: Repairability index 2.3 - France
+fr: Indice de réparabilité 2.3
+repairability_index_france_value:en: 2.3
+
+en: Repairability index 2.4 - France
+fr: Indice de réparabilité 2.4
+repairability_index_france_value:en: 2.4
+
+en: Repairability index 2.5 - France
+fr: Indice de réparabilité 2.5
+repairability_index_france_value:en: 2.5
+
+en: Repairability index 2.6 - France
+fr: Indice de réparabilité 2.6
+repairability_index_france_value:en: 2.6
+
+en: Repairability index 2.7 - France
+fr: Indice de réparabilité 2.7
+repairability_index_france_value:en: 2.7
+
+en: Repairability index 2.8 - France
+fr: Indice de réparabilité 2.8
+repairability_index_france_value:en: 2.8
+
+en: Repairability index 2.9 - France
+fr: Indice de réparabilité 2.9
+repairability_index_france_value:en: 2.9
+
+en: Repairability index 3.0 - France
+fr: Indice de réparabilité 3.0
+repairability_index_france_value:en: 3.0
+
+en: Repairability index 3.1 - France
+fr: Indice de réparabilité 3.1
+repairability_index_france_value:en: 3.1
+
+en: Repairability index 3.2 - France
+fr: Indice de réparabilité 3.2
+repairability_index_france_value:en: 3.2
+
+en: Repairability index 3.3 - France
+fr: Indice de réparabilité 3.3
+repairability_index_france_value:en: 3.3
+
+en: Repairability index 3.4 - France
+fr: Indice de réparabilité 3.4
+repairability_index_france_value:en: 3.4
+
+en: Repairability index 3.5 - France
+fr: Indice de réparabilité 3.5
+repairability_index_france_value:en: 3.5
+
+en: Repairability index 3.6 - France
+fr: Indice de réparabilité 3.6
+repairability_index_france_value:en: 3.6
+
+en: Repairability index 3.7 - France
+fr: Indice de réparabilité 3.7
+repairability_index_france_value:en: 3.7
+
+en: Repairability index 3.8 - France
+fr: Indice de réparabilité 3.8
+repairability_index_france_value:en: 3.8
+
+en: Repairability index 3.9 - France
+fr: Indice de réparabilité 3.9
+repairability_index_france_value:en: 3.9
+
+en: Repairability index 4.0 - France
+fr: Indice de réparabilité 4.0
+repairability_index_france_value:en: 4.0
+
+en: Repairability index 4.1 - France
+fr: Indice de réparabilité 4.1
+repairability_index_france_value:en: 4.1
+
+en: Repairability index 4.2 - France
+fr: Indice de réparabilité 4.2
+repairability_index_france_value:en: 4.2
+
+en: Repairability index 4.3 - France
+fr: Indice de réparabilité 4.3
+repairability_index_france_value:en: 4.3
+
+en: Repairability index 4.4 - France
+fr: Indice de réparabilité 4.4
+repairability_index_france_value:en: 4.4
+
+en: Repairability index 4.5 - France
+fr: Indice de réparabilité 4.5
+repairability_index_france_value:en: 4.5
+
+en: Repairability index 4.6 - France
+fr: Indice de réparabilité 4.6
+repairability_index_france_value:en: 4.6
+
+en: Repairability index 4.7 - France
+fr: Indice de réparabilité 4.7
+repairability_index_france_value:en: 4.7
+
+en: Repairability index 4.8 - France
+fr: Indice de réparabilité 4.8
+repairability_index_france_value:en: 4.8
+
+en: Repairability index 4.9 - France
+fr: Indice de réparabilité 4.9
+repairability_index_france_value:en: 4.9
+
+en: Repairability index 5.0 - France
+fr: Indice de réparabilité 5.0
+repairability_index_france_value:en: 5.0
+
+en: Repairability index 5.1 - France
+fr: Indice de réparabilité 5.1
+repairability_index_france_value:en: 5.1
+
+en: Repairability index 5.2 - France
+fr: Indice de réparabilité 5.2
+repairability_index_france_value:en: 5.2
+
+en: Repairability index 5.3 - France
+fr: Indice de réparabilité 5.3
+repairability_index_france_value:en: 5.3
+
+en: Repairability index 5.4 - France
+fr: Indice de réparabilité 5.4
+repairability_index_france_value:en: 5.4
+
+en: Repairability index 5.5 - France
+fr: Indice de réparabilité 5.5
+repairability_index_france_value:en: 5.5
+
+en: Repairability index 5.6 - France
+fr: Indice de réparabilité 5.6
+repairability_index_france_value:en: 5.6
+
+en: Repairability index 5.7 - France
+fr: Indice de réparabilité 5.7
+repairability_index_france_value:en: 5.7
+
+en: Repairability index 5.8 - France
+fr: Indice de réparabilité 5.8
+repairability_index_france_value:en: 5.8
+
+en: Repairability index 5.9 - France
+fr: Indice de réparabilité 5.9
+repairability_index_france_value:en: 5.9
+
+en: Repairability index 6.0 - France
+fr: Indice de réparabilité 6.0
+repairability_index_france_value:en: 6.0
+
+en: Repairability index 6.1 - France
+fr: Indice de réparabilité 6.1
+repairability_index_france_value:en: 6.1
+
+en: Repairability index 6.2 - France
+fr: Indice de réparabilité 6.2
+repairability_index_france_value:en: 6.2
+
+en: Repairability index 6.3 - France
+fr: Indice de réparabilité 6.3
+repairability_index_france_value:en: 6.3
+
+en: Repairability index 6.4 - France
+fr: Indice de réparabilité 6.4
+repairability_index_france_value:en: 6.4
+
+en: Repairability index 6.5 - France
+fr: Indice de réparabilité 6.5
+repairability_index_france_value:en: 6.5
+
+en: Repairability index 6.6 - France
+fr: Indice de réparabilité 6.6
+repairability_index_france_value:en: 6.6
+
+en: Repairability index 6.7 - France
+fr: Indice de réparabilité 6.7
+repairability_index_france_value:en: 6.7
+
+en: Repairability index 6.8 - France
+fr: Indice de réparabilité 6.8
+repairability_index_france_value:en: 6.8
+
+en: Repairability index 6.9 - France
+fr: Indice de réparabilité 6.9
+repairability_index_france_value:en: 6.9
+
+en: Repairability index 7.0 - France
+fr: Indice de réparabilité 7.0
+repairability_index_france_value:en: 7.0
+
+en: Repairability index 7.1 - France
+fr: Indice de réparabilité 7.1
+repairability_index_france_value:en: 7.1
+
+en: Repairability index 7.2 - France
+fr: Indice de réparabilité 7.2
+repairability_index_france_value:en: 7.2
+
+en: Repairability index 7.3 - France
+fr: Indice de réparabilité 7.3
+repairability_index_france_value:en: 7.3
+
+en: Repairability index 7.4 - France
+fr: Indice de réparabilité 7.4
+repairability_index_france_value:en: 7.4
+
+en: Repairability index 7.5 - France
+fr: Indice de réparabilité 7.5
+repairability_index_france_value:en: 7.5
+
+en: Repairability index 7.6 - France
+fr: Indice de réparabilité 7.6
+repairability_index_france_value:en: 7.6
+
+en: Repairability index 7.7 - France
+fr: Indice de réparabilité 7.7
+repairability_index_france_value:en: 7.7
+
+en: Repairability index 7.8 - France
+fr: Indice de réparabilité 7.8
+repairability_index_france_value:en: 7.8
+
+en: Repairability index 7.9 - France
+fr: Indice de réparabilité 7.9
+repairability_index_france_value:en: 7.9
+
+en: Repairability index 8.0 - France
+fr: Indice de réparabilité 8.0
+repairability_index_france_value:en: 8.0
+
+en: Repairability index 8.1 - France
+fr: Indice de réparabilité 8.1
+repairability_index_france_value:en: 8.1
+
+en: Repairability index 8.2 - France
+fr: Indice de réparabilité 8.2
+repairability_index_france_value:en: 8.2
+
+en: Repairability index 8.3 - France
+fr: Indice de réparabilité 8.3
+repairability_index_france_value:en: 8.3
+
+en: Repairability index 8.4 - France
+fr: Indice de réparabilité 8.4
+repairability_index_france_value:en: 8.4
+
+en: Repairability index 8.5 - France
+fr: Indice de réparabilité 8.5
+repairability_index_france_value:en: 8.5
+
+en: Repairability index 8.6 - France
+fr: Indice de réparabilité 8.6
+repairability_index_france_value:en: 8.6
+
+repairability_index_france_value:en: 8.7
+
+en: Repairability index 8.8 - France
+fr: Indice de réparabilité 8.8
+repairability_index_france_value:en: 8.8
+
+en: Repairability index 8.9 - France
+fr: Indice de réparabilité 8.9
+repairability_index_france_value:en: 8.9
+
+en: Repairability index 9.0 - France
+fr: Indice de réparabilité 9.0
+repairability_index_france_value:en: 9.0
+
+en: Repairability index 9.1 - France
+fr: Indice de réparabilité 9.1
+repairability_index_france_value:en: 9.1
+
+en: Repairability index 9.2 - France
+fr: Indice de réparabilité 9.2
+repairability_index_france_value:en: 9.2
+
+en: Repairability index 9.3 - France
+fr: Indice de réparabilité 9.3
+repairability_index_france_value:en: 9.3
+
+en: Repairability index 9.4 - France
+fr: Indice de réparabilité 9.4
+repairability_index_france_value:en: 9.4
+
+en: Repairability index 9.5 - France
+fr: Indice de réparabilité 9.5
+repairability_index_france_value:en: 9.5
+
+en: Repairability index 9.6 - France
+fr: Indice de réparabilité 9.6
+repairability_index_france_value:en: 9.6
+
+en: Repairability index 9.7 - France
+fr: Indice de réparabilité 9.7
+repairability_index_france_value:en: 9.7
+
+en: Repairability index 9.8 - France
+fr: Indice de réparabilité 9.8
+repairability_index_france_value:en: 9.8
+
+en: Repairability index 9.9 - France
+fr: Indice de réparabilité 9.9
+repairability_index_france_value:en: 9.9
+
+en: Repairability index 10 - France
+fr: Indice de réparabilité 10
+repairability_index_france_value:en: 10
+
diff --git a/taxonomies/test.txt b/taxonomies/test.txt
index 306f3afdc3ba7..a13b180085932 100644
--- a/taxonomies/test.txt
+++ b/taxonomies/test.txt
@@ -123,5 +123,9 @@ de: Special value for German 3
sv: Ä-märket
xx: Ä-märket
+en: Smartphones
+xx: Smartphones
+fr: Téléphones intelligents
+
en: Entry with (parentheses) and some *!#{}@$ characters, synonym with *%@$(]% chars
diff --git a/templates/api/knowledge-panels/environment/carbon_footprint.tt.json b/templates/api/knowledge-panels/environment/carbon_footprint_food.tt.json
similarity index 100%
rename from templates/api/knowledge-panels/environment/carbon_footprint.tt.json
rename to templates/api/knowledge-panels/environment/carbon_footprint_food.tt.json
diff --git a/templates/api/knowledge-panels/environment/carbon_footprint_product.tt.json b/templates/api/knowledge-panels/environment/carbon_footprint_product.tt.json
new file mode 100644
index 0000000000000..725521cc409e0
--- /dev/null
+++ b/templates/api/knowledge-panels/environment/carbon_footprint_product.tt.json
@@ -0,0 +1,35 @@
+[% SET driving_per_unit = panel.co2_kg_per_unit * 100 / 19.3 %]
+[% SET driving_per_unit_rounded = sprintf('%.1f', driving_per_unit) %]
+[% SET co2_kg_per_unit_rounded = sprintf('%.1f', panel.co2_kg_per_unit) %]
+{
+ "level" :"info",
+ "topics": [
+ "environment"
+ ],
+ "expanded": false,
+ "title_element": {
+ "title": "1 [% panel.unit_name %] = [% co2_kg_per_unit_rounded %] kg CO₂e",
+ "subtitle": "[% edq(lang("average_for_the_category").ucfirst) %][% sep %]: [% panel.category_name %]",
+ "icon_url": "[% static_subdomain %]/images/panels/fr-impact-co2/impact-co2.svg",
+ },
+ "elements": [
+ {
+ "element_type": "text",
+ "text_element": {
+ "type": "summary",
+ "html": `
+