-
Notifications
You must be signed in to change notification settings - Fork 144
Abstraction for Multilingual Content #6109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Multilingual objectsWe need two new classes that each inherits from
Example code for MultilingualContent/**
* @property-read int $objectID
* @property-read ?int $languageID
*/
abstract class MultilingualContent extends DatabaseObject
{
public static function getContent(int $objectID, ?int $languageID): ?static
{
if ($languageID === null) {
$languageID = WCF::getLanguage()->languageID;
}
$defaultLanguageID = LanguageFactory::getInstance()->getDefaultLanguage()->languageID;
$contentTableName = static::getDatabaseTableName();
$statement = WCF::getDB()->prepare(
<<<SQL
SELECT *
FROM {$contentTableName}
WHERE objectID = ?
ORDER BY CASE
WHEN languageID = {$languageID} THEN -2
WHEN languageID = {$defaultLanguageID} THEN -1
ELSE languageID
END ASC
LIMIT 1
SQL
);
$statement->execute([$objectID]);
return $statement->fetchObject(static::class);
}
}
Example code for Multilingual/**
* @template TContent of MultilingualContent
*
* @property-read int $isMultilingual
*/
abstract class Multilingual extends DatabaseObject
{
/**
* @var class-string<TContent>
*/
protected static string $contentClassName;
/**
* @var array<int, TContent>
*/
protected array $contents;
/**
* @param TContent $content
*/
public function setContent(MultilingualContent $content): void
{
if (!isset($this->contents)) {
$this->contents = [];
}
$this->contents[$content->languageID ?: 0] = $content;
}
/**
* @return ?TContent
*/
public function getContent(?int $languageID = null): ?MultilingualContent
{
$this->loadContent();
if ($this->isMultilingual === 0) {
if (isset($this->contents[0])) {
return $this->contents[0];
}
} else {
if ($languageID === null) {
$languageID = WCF::getLanguage()->languageID;
}
return $this->contents[$languageID]
?? $this->contents[LanguageFactory::getInstance()->getDefaultLanguageID()]
?? \reset($this->contents);
}
return null;
}
protected function loadContent(): void
{
if (!isset($this->contents)) {
$this->contents = [];
$contentTableName = static::getContentClassName()::getDatabaseTableName();
$statement = WCF::getDB()->prepare(
<<<SQL
SELECT *
FROM {$contentTableName}
WHERE objectID = ?
SQL
);
$statement->execute([$this->getObjectID()]);
while ($content = $statement->fetchObject(static::getContentClassName())) {
$this->contents[$content->languageID ?: 0] = $content;
}
if ($this->isMultilingual) {
\ksort($this->contents);
}
}
}
/**
* @return class-string<TContent>
*/
public static function getContentClassName(): string
{
return static::$contentClassName;
}
} Database Table EditorTo create the database table, a helper class should be provided, which already inserts Database object listA sorted or filtered list of objects ( A function should be provided which loads the The SQL query could then look like thisSELECT foo.*
FROM (
SELECT foo.*, (
SELECT languageID
FROM wcf1_foo_content fooContent
WHERE foo.fooID = fooContent.objectID
ORDER BY CASE
WHEN languageID = ? THEN -2 -- preferred languageID
WHEN languageID = ? THEN -1 -- default languageID
ELSE languageID
END ASC
LIMIT 1
) AS languageID
FROM wcf1_foo foo
) as foo
INNER JOIN wcf1_foo_content fooContent
ON foo.fooID = fooContent.objectID
AND foo.languageID = fooContent.languageID
WHERE fooContent.title LIKE ?
ORDER BY fooContent.secondColumn DESC Object Action helperA command should be called when Example for createabstract class MultilingualDatabaseObjectAction extends AbstractDatabaseObjectAction
{
/**
* @var class-string
*/
protected string $createContentCommand;
#[\Override]
public function create()
{
$object = parent::create();
if (isset($this->createContentCommand) && isset($this->parameters['content'])) {
$objectID = $object->getObjectID();
foreach ($this->parameters['content'] as $languageID => $content) {
$parameters = \array_merge([
"objectID" => $objectID,
"languageID" => $languageID ?: null,
], $content);
(new $this->createContentCommand(...$parameters))();
}
}
return $object;
}
} A command MUST provide the parameters final class CreateFooContent{
public function __construct(
public readonly int $objectID,
public readonly ?int $languageID,
// extra defined properties
public readonly string $title,
public readonly string $description,
// everything else
...$extra
) {
}
public function __invoke(): void
{
// do something
}
} Form BuilderA
All fields with |
Thinking about it a while, there is possibly little value in providing a concrete implementation, at least in terms of a rigid implementation like the proposed DBO classes. I’m also not convinced that the At this point I’m leaning towards not providing an abstract implementation because there is little added value while at the same time increasing complexity by a lot. That doesn’t mean that we won’t provide anything but instead document the concept and provide helpful code pieces. This simply isn’t a problem that can be meaningfully solved by abstraction. The best approach would be to implement this concept for two existing pieces and then in retrospective look if there are meaningful gains in providing any sort of helpers. Even if there are none, this could still surface later on and can then be introduced when the need is there. |
We currently uses multilingual content in different components, and this content is stored in different ways. I18nHandler is used by:
An separate table is used by:
|
There are multiple different implementations for multilingual content and they are all either flawed to some extent or quite limited or both. Some store there values in phrases, others use separate tables but are inconsistent on how the ids are mapped. Furthermore being able to mix monolingual and multilingual content creates a lot of issues when sorting, filtering or generally trying to work with those values because it involves some kind of magic, for example, the value could be a plain value or the name of a phrase.
The only solution forward is a consistent abstraction that handles this in a uniform way that does not rely on phrases (which was a stop-gap solution in itself) and provides convenient helper methods to work with them. It needs to solve the following problems:
languageID
.Determining the values can be done through a sub select using either
CASE … THEN
or by assigning thelanguageID
a value by preference. The latter could be achieved by setting-2
for preferred language,-1
for default language and the actual languageID for everything else followed by then selecting the lowest value.This requires extra tables for any such content and should be entirely managed like it is already the case with the
*_search_index
tables.objectID
andlanguageID
need to be fixed and all other columns could be represented through a list ofIDatabaseTableColumn
. Additionally this would set up a foreign key for both thelanguageID
as well as theobjectID
with the latter referencing the original object.We need to explore if this is feasible through specialized helper methods to avoid API consumers having to write custom queries to work with these values.
The text was updated successfully, but these errors were encountered: