Problem/Motivation

EntityType::getLowercaseLabel() fetches the translated label and then runs a strtolower() on that.

The usage of EntityType::getLowercaseLabel() is for using entity type labels in a sentence, for example:

This is a block!

can be generated by

t('This is a @label!', ['@label' => $entity_type->getLowercaseLabel()]);

This breaks translation in some languages, however. In German, for example, nouns are always uppercase, so for the sentence to be correct, it must be

Das ist ein Block!

not

Das ist ein block!

It is not possible to generate the first string with the current API in German.

Proposed resolution

We can go with
(A) use new entity type labels and/or
(B) more keys in the $args parameter of the t() function to allow the translator to decide.
(C) add context to the t() function to allow the translator to decide.

Remaining tasks

Decide which resolution to take.

User interface changes

No new functions or buttons, but more correct translations.

API changes

Deprecate getLowercaseLabel().

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Arla’s picture

Version: 8.0.x-dev » 9.x-dev

In order to have both cases ("Block" and "block") translatable, both strings have to be defined in code. The capitalization rules are not specific to particular entity types, so it would have to be defined by all entity types. Like in #2604902: Allow entity types to override EntityType::getLowercaseLabel(), an additional lowercase_label in the @ContentEntityType/@ConfigEntityType annotation would do it, but in contrast to that issue, the key would not be optional. A required annotation key would break APIs so I'm switching to 9.x.

tstoeckler’s picture

Version: 9.x-dev » 8.0.x-dev

I think that's a bit premature, to be honest. On German sites, this is a pretty big nuisance, because it makes Drupal look sloppy and unprofessional. The tabs added by the Configuration Translation module, for instance, are

inhaltstyp übersetzen

in German, which looks like it is a typo (it should be Inhaltstyp übersetzen). Even more confusing, when a user tries to fix the apparently broken string on the user interface translation page, she will have no luck finding the string "inhaltstyp" there.

So the problem in German is comparable to if the string would erroneously be "Tranlsate content type" in English (except that that would be a much easier fix).

Also, I don't think we need any API break for this. As proposed in #2604902: Allow entity types to override EntityType::getLowercaseLabel(), I think a lowercase_label in the entity type annotation would allow fixing this, but we do not have to make it required. We can simply use it if it is there and fall back to the current behavior if not.

Arla’s picture

Oh, the "inhaltstyp übersetzen" example indicates a new aspect of the problem. Or actually two new aspects.

  1. In the pattern t('Translate @entity_type', ['@entity_type' => $entity_type->getLowercaseLabel()]), the capitalization decision is dependent on the word order which of course is very language-dependent.
  2. In the mentioned Configuration Translation tab, the lowercasing is not even delegated to getLowercaseLabel():
    $type_name = Unicode::strtolower($this->mapperManager()->createInstance($this->pluginDefinition['config_translation_plugin_id'])->getTypeLabel());
    return $this->t('Translate @type_name', array('@type_name' => $type_name));
    

I'm really not sure how to deal with that.

tstoeckler’s picture

Oh right! Nice find. I recently saw that string and blindly assumed it was this bug. Thanks! And yes, the order makes it even more complex. :-/

Berdir’s picture

Duplicate of #2600806: Remove all invalid lowercase transformations from translatable strings.

I honestly have no idea how to solve this as already mentioned there, what @hass is doing there is obviously not correct.

IMHO, the easiest fix for config_translation would be to just stop using the type in there. It's usually also just "Edit" and "Delete" etc. local tasks, and not "Edit content".

Berdir’s picture

Idea: What about introducing a new placeholder type that can upper/lowercase automatically based on the language? Something like *label. Then we can lowercase based on a flag on the language?

Arla’s picture

I though of a placeholder prefix as well. I'm really not sure about the consequences, but I suspect that there's a large and unpredictable number of similar parameters to placeholder substitution, like for instance noun cases and plural (in languages where these exist and are not simple affixes).

Also, this solution kind of conflicts with the discussion in #2604902: Allow entity types to override EntityType::getLowercaseLabel(). If TranslationInterface::translateString() takes over the responsibility to determine capitalization from the placeholder type, how do we make sure it doesn't just lowercase "RDF Mapping" and "JW Player" to "rdf mapping" and "jw player"?

Edit: These problems are not specific to Drupal, but i18n in general. Aren't there books written on the subject, or at least some pattern to adhere to? :)

bojanz’s picture

We have two options
a) Convert labels to lowercase, and then uppercase them when needed
b) Introduce an optional lowercase_label key. If missing, the current logic is used.

It's too late for a). b) can be done in a backwards compatible way.

Arla’s picture

Status: Active » Needs review
FileSize
21.05 KB

Took the patch from #2604902: Allow entity types to override EntityType::getLowercaseLabel() and added the lowercase_label key to all non-test entity types. Also adding a context to all lowercase_label @Translation objects.

tstoeckler’s picture

Patch looks good to me. I didn't really play around with it, so I can't really RTBC. Also the first item needs feedback from someone who knows POTX.

Some comments:

  1. +++ b/core/lib/Drupal/Core/Entity/EntityType.php
    @@ -286,6 +296,18 @@ public function __construct($definition) {
    +    // Add a context to the lowercase label translation object. This makes sure
    +    // that the lowercase label is translatable independently from the normal
    +    // label. For example, "RDF mapping" can have the label translated to French
    +    // as "Correspondance RDF" and the lowercase label as "correspondance RDF".
    +    if (isset($this->lowercase_label) && $this->lowercase_label instanceof TranslatableMarkup) {
    +      $this->lowercase_label = new TranslatableMarkup(
    +        $this->lowercase_label->getUntranslatedString(),
    +        $this->lowercase_label->getArguments(),
    +        $this->lowercase_label->getOptions() + ['context' => 'Entity type lowercase label']
    +      );
    +    }
    
    +++ b/core/modules/rdf/src/Entity/RdfMapping.php
    @@ -17,6 +17,7 @@
      *   label = @Translation("RDF mapping"),
    + *   lowercase_label = @Translation("RDF mapping"),
    

    This makes a lot of sense, nice one!

    I'm not very familiar (at all) with POTX, will it cause any problem that POTX will not be able to find the context? I.e. maybe we just have to add the context to all lowercase labels manually?

    Can we add a comment (and a @see?) to the RdfMapping class docs pointing to EntityType::__construct().

  2. +++ b/core/modules/views/src/Entity/View.php
    @@ -21,6 +21,7 @@
      *   label = @Translation("View", context = "View entity type"),
    + *   lowercase_label = @Translation("view", context = "View entity type lowercase"),
    

    In theory I think the generic "Entity type lowercase label" should be sufficient context here, but I can see that it is done here for consistency, which makes sense.

    In the same light the uppercase label should really just have "Entity type label" as context, but it's out of scope to change this here (and in 8.x in general). For D9 we could do the same trick with the entity type labels itself (i.e. always add a "Entity type label" context by default) so we could remove this from the annotation here completely.

    So nothing actionable, just thinking out loud.

  3. +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php
    @@ -299,6 +299,28 @@ public function testGetLabel() {
    +    $this->assertSame('thing abc', $entity_type = $entity_type->getLowercaseLabel());
    ...
    +    $this->assertSame('thing ABC', $entity_type = $entity_type->getLowercaseLabel());
    

    I think the $entity_type = can be dropped from these lines.

Gábor Hojtsy’s picture

Status: Needs review » Needs work
Issue tags: +language-ui, +sprint

It would be nice to figure out how will this work if/once #1850080: Entity type labels lack plurality, cannot generate UI text based on label if plural is needed is added too. You don't need to resolve it here, just saying that is also in the works.

+++ b/core/lib/Drupal/Core/Entity/EntityType.php
@@ -286,6 +296,18 @@ public function __construct($definition) {
+    // Add a context to the lowercase label translation object. This makes sure
+    // that the lowercase label is translatable independently from the normal
+    // label. For example, "RDF mapping" can have the label translated to French
+    // as "Correspondance RDF" and the lowercase label as "correspondance RDF".
+    if (isset($this->lowercase_label) && $this->lowercase_label instanceof TranslatableMarkup) {
+      $this->lowercase_label = new TranslatableMarkup(
+        $this->lowercase_label->getUntranslatedString(),
+        $this->lowercase_label->getArguments(),
+        $this->lowercase_label->getOptions() + ['context' => 'Entity type lowercase label']
+      );
+    }

There is no way to find this context with static code analysis, so no way for localize.drupal.org to find it. Just include it in the annotation.

Berdir’s picture

Version: 8.0.x-dev » 8.1.x-dev

Looks like #1850080: Entity type labels lack plurality, cannot generate UI text based on label if plural is needed went with something that does not complicate this, as they only use lowercase labels.

But yes, Gabor is right. The automatic lowercase thing does *not* help. We need to manually, explicitly provide lowercase labels for all core entity types.

We do still need a runtime fallback in getLowerCaseLabel() I think. And I still think it might be better to use the approach above, so it *can* be translated, at least after it was looked at, instead of not being able to do so at all.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Gábor Hojtsy’s picture

Issue tags: -sprint

Removing from sprint so it reflects what is currently being worked on.
Unfortunately.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

ckaotik’s picture

This is an issue that is spread all across core and also seeps into contrib modules that use core implementations as a guideline. The sooner we can get this fixed, the better.

The issue has been stagnant for over a year, what are the next steps to get this going?

Berdir’s picture

Certainly needs a reroll and then trying to address the feedback in the last few comments.

tstoeckler’s picture

Is this not now handled by the label_singular annotation?

ckaotik’s picture

Is this not now handled by the label_singular annotation?

No, this is a different problem. A single node of type "Article" is referred to as "article" in English texts, but the same node in German (of type "Artikel") needs to be referred to as "Artikel" (because in German, nouns are always capitalized).
Having a separate lowercase label would allow the site builder/translator to capitalize the label as appropriate, though "lowercase label" might be confusing naming here. Think of it as an "in a full sentence, this is what we call this thing" label.

Arla also described another aspect in #3, where capitalization additionally depends on word order (e.g. sentences starting with a capital letter). But I suppose that is worth a separate issue.

tstoeckler’s picture

Think of it as an "in a full sentence, this is what we call this thing" label.

Well that pretty much is the singular label, no?

Can you give an example where label_singular !== lowercase_label?

ckaotik’s picture

Well, if you put it this way ... It does make sense. (Unless I'm completely missing something here.) Seems this was introduced a year ago, if the change record is any indication.

That means we'd probably have to supply these new labels to all core entity types still (after a quick search, I could only find them specified for node, comment, comment_type, rdf_mapping and content_moderation_state) and try to weed out all places where $entity_type->getLowercaseLabel(), strtolower() or Unicode::strtolower() are incorrectly used instead of getSingularLabel() etc.

Gábor Hojtsy’s picture

Yay, thanks sounds like a good direction :)

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

ckaotik’s picture

Status: Needs work » Needs review
FileSize
17.51 KB

I've finally gotten around to looking into some of the cases mentioned in #22:

  • $entity_type->getLowercaseLabel() - checked
  • mb_strtolower - checked
  • strtolower - not checked
  • Unicode::strtolower - not checked

As for the lowercase_label annotation and its friends - they have already been added in the meantime.
Most of the changes are straightforward, however there are some things that are not as easily solved and need some more insights.

  1. \Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldValueValidator lowercases @field_name, but seems to not be used in Core.
  2. \Drupal\config_translation\Controller\ConfigTranslationFieldListBuilder::getFilterLabels

    Is problematic, because a) we cannot get $entity_type->bundle_label (which is likely always capitalized and thus undesirable in English), and b) if it's not set, \Drupal\Core\Entity\EntityType::getBundleLabel() returns getLabel() where we'd like getSingularLabel()
  3. Configuration Translation plugins with getType()
    \Drupal\config_translation\Plugin\Menu\ContextualLink\ConfigTranslationContextualLink::getTitle and
    \Drupal\config_translation\Plugin\Menu\LocalTask\ConfigTranslationLocalTask::getTitle,
    also within ConfigTranslationUiTest.

    Is problematic, because translation definitions only have one label (no singular/plural forms) - and it's used both for page/row titles (/admin/config/regional/config-translation) where capitalization is wanted, but also within phrases (e.g. "Translate x" tabs) where lowercasing is not wanted.

    I would suggest continuing this separately in #2275835: Capitalisation of translation tab forced to lowercase.

  4. \Drupal\language\Form\NegotiationConfigureForm::configureFormTable lowercases the negotiation message name

Status: Needs review » Needs work

The last submitted patch, 27: 2507235-27.patch, failed testing. View results

Gogowitsch’s picture

Issue summary: View changes

Here in this issue, there seems to be consensus that the entity type has to have even more keys. I propose to use a more light-way approach that doesn't break anything and does not add burden to all entity types.

Please have a look at the patch and approach outlined in #3025855. @tstoeckler correctly marked it as a duplicate, so let's continue the discussion here in #2507235.

I agree that this issue in general is quite important for the public image of Drupal in Germany.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

ckaotik’s picture

@Gogowitsch The thing is, we're not adding more entity annotation keys in this issue - that's already happened in core (see the change record). So while having tons and tons of things to set in an entity type's annotation isn't pretty, it's what core is doing. Changing that should probably get its own issue.

I've had two ideas regarding my notes in #27, that might be worth discussing for labels that don't currently use singular/plural forms.

1) We could (automatically when parsing the entity?) add a context to the string translation (e.g. "entity type label", "bundle label") to prevent accidental translation. Example: Labels that can be either verb or noun, like "exit" (in German: "Ausgang" = the exit/"verlassen" or "beenden" = to exit), where a generic translation will probably differ from the intended meaning.

2) Currently, we use capitalized labels as defaults, and in certain cases defer to lowercased versions. What if, instead, we used the lowercase version as default and capitalize on a per-language per-case base? E.g. for config translation labels we use the label as-is on the local task (tab label "Translate @type_name") or in other cases where we can't be sure about capitalization, and capitalize it for standalone cases (i.e. when it's not a placeholder in a t-string), like the row title on the config translation overview page?
I'm not sure how this works for other languages, but iirc most european languages are fine with capitalized labels, as in standalone headings. The problem mostly comes from (not) using capitalization within phrases/sentences.

ckaotik’s picture

Rerolled patch from #27 against 8.7. Needs to be ported to 8.8, and remarks from before are still open.

ckaotik’s picture

ckaotik’s picture

tstoeckler’s picture

Status: Needs review » Needs work

Thanks for the patch. It looks great. I think you missed one case in UniqueFieldConstraintTest.

Also I think we should use this issue to deprecate getLowercaseLabel(). We should add a deprecation notice and simply inline the final remaining call in getSingularLabel(). Can you do the deprecation according to https://www.drupal.org/core/deprecation @ckaotik ? I am really glad to see you pushing this along and it would be awesome to see this land!

Gogowitsch’s picture

Thanks to @ckaotik, @tstoeckler and all the others for the work on this issue.

I went ahead and created an updated patch for 8.8.x. In it, I:

  • replaced further getLowercaseLabel() calls with getSingularLabel()
  • deprecated getLowercaseLabel().
  • added a line in the DocBlock comments of EntityTypeInterface to explicitly state that the return value from getSingularLabel() will be used in (the middle of) a sentence.
  • changed getSingularLabel to call mb_strtolower directly.

I could not find any additional problematic call to strtolower() for entity types. There is code in core/modules/views/src/Plugin/views/style/Mapping.php:69 that might cause problems in German for field names, but I'll ignore that and focus on entity labels only.

Status: Needs review » Needs work
Gogowitsch’s picture

Ok, the new deprecation caused the tests to fail. That's actually good news, meaning that the tests pick it up.

This time I attached a patch that should get a green light from the Drupal Core tests.

tstoeckler’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record

Awesome work @Gogowitsch! 👍🏿 Thanks a lot. Yes, the previous test fail ist great verification of the patch.

One thing needs work, however, unfortunately. We need to add a change notice for the deprecation and then link to that from the deprecation instead of this issue. You can use the "add change notice" link at the top right of the page.

Sorry, I could have made that clearer by adding the respektive tag, doing that now.

Gogowitsch’s picture

No problem.

I wrote a draft change record - feedback is very welcome. In the attached patch, we now link to that new change record instead of this issue.

Send with love from the Extinction Rebellion 💚 bus :)

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Thanks a lot! Looks great to me.

catch’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Entity/EntityType.php
@@ -775,6 +775,7 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
+    @trigger_error('EntityType::getLowercaseLabel() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Instead, you should call getSingularLabel() to prevent breaking the German translation. See https://www.drupal.org/node/3075567', E_USER_DEPRECATED);

I think we can remove 'to prevent breaking the German translation' here and rely on the change notice and commit history to convey why.

Gogowitsch’s picture

Thanks, @catch!

Since the last patch I removed the explanation. Now, the deprecation message is clearer.

  • catch committed ed71ff5 on 8.8.x
    Issue #2507235 by Gogowitsch, ckaotik, Arla, tstoeckler, Berdir, Gábor...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed ed71ff5 and pushed to 8.8.x. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.