diff --git a/.changes/v4.10.0.md b/.changes/v4.10.0.md new file mode 100644 index 000000000..e3ecfbf24 --- /dev/null +++ b/.changes/v4.10.0.md @@ -0,0 +1,6 @@ +## v4.10.0 - 2025-12-09 +### Feature +* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results +### Fixed +* Remove dependency to package @symfony/ux-translator + diff --git a/.changes/v4.10.1.md b/.changes/v4.10.1.md new file mode 100644 index 000000000..410d89bf0 --- /dev/null +++ b/.changes/v4.10.1.md @@ -0,0 +1,6 @@ +## v4.10.1 - 2025-12-11 +### Fixed +* Fix missing translation variable in NewLocation component +* ([#476](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/476)) Fix display of header for ByActivityNumberAggregator +* Fix use of ByActivityNumberAggregator in combination with activity count exports +* ([#483](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/483)) Tentatively fix usage of CTRL+C in collabora editor with chrome / edge browser diff --git a/CHANGELOG.md b/CHANGELOG.md index e4669a037..5700c47cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v4.10.1 - 2025-12-11 +### Fixed +* Fix missing translation variable in NewLocation component +* ([#476](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/476)) Fix display of header for ByActivityNumberAggregator +* Fix use of ByActivityNumberAggregator in combination with activity count exports +* ([#483](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/483)) Tentatively fix usage of CTRL+C in collabora editor with chrome / edge browser + +## v4.10.0 - 2025-12-09 +### Feature +* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results +### Fixed +* Remove dependency to package @symfony/ux-translator + + ## v4.9.0 - 2025-12-05 ### Feature * ([#459](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/459)) Add a counter for invitations awaiting reply diff --git a/composer.json b/composer.json index 9f641bb1b..ef3185191 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,6 @@ "ext-openssl": "*", "ext-redis": "*", "ext-zlib": "*", - "chill-project/chill-zimbra-bundle": "@dev", "champs-libres/wopi-bundle": "dev-symfony-v5@dev", "champs-libres/wopi-lib": "dev-master@dev", "doctrine/data-fixtures": "^1.8", @@ -62,7 +61,6 @@ "symfony/http-client": "^5.4", "symfony/http-foundation": "^5.4", "symfony/intl": "^5.4", - "symfony/loco-translation-provider": "^6.0", "symfony/mailer": "^5.4", "symfony/messenger": "^5.4", "symfony/mime": "^5.4", @@ -119,7 +117,8 @@ "symfony/runtime": "^5.4", "symfony/stopwatch": "^5.4", "symfony/var-dumper": "^5.4", - "symfony/web-profiler-bundle": "^5.4" + "symfony/web-profiler-bundle": "^5.4", + "symfony/loco-translation-provider": "^6.0" }, "conflict": { "symfony/symfony": "*" diff --git a/config/bundles.php b/config/bundles.php index 021622126..7fd27811d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -38,5 +38,4 @@ return [ Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true], loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true], - Chill\ZimbraBundle\ChillZimbraBundle::class => ['all' => true], ]; diff --git a/config/services.yaml b/config/services.yaml index 11c095d99..037b57f6d 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -11,3 +11,6 @@ services: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. +when@dev: + services: + ChampsLibres\WopiLib\Contract\Service\ProofValidatorInterface: '@Chill\WopiBundle\Service\Wopi\NullProofValidator' diff --git a/docs/source/development/index.md b/docs/source/development/index.md index ab763538c..bb685dbd2 100644 --- a/docs/source/development/index.md +++ b/docs/source/development/index.md @@ -12,6 +12,8 @@ As Chill relies on the [symfony ](http://symfony.com) framework, reading the fra - [Messages to users](messages-to-users.md) - [Pagination](pagination.md) - [Localisation](localisation.md) +- [Translation directives](translation_directives.md) +- [Translation provider](translation_provider.md) - [Logging](logging.md) - [Database migrations](migrations.md) - [Searching](searching.md) diff --git a/docs/source/development/translation_directives.md b/docs/source/development/translation_directives.md new file mode 100644 index 000000000..ce7561904 --- /dev/null +++ b/docs/source/development/translation_directives.md @@ -0,0 +1,376 @@ +# Translation Key Directives + +These directives are meant to ensure better consistency across bundles, avoid duplication, and make keys more predictable. + +## General Principles + +1. **Use lowercase snake_case for all keys** + +2. **Use dot-separated namespaces** + The dot is used to reflect: + - bundle + - feature + - sub-feature + - key type + +3. **Do not use spaces in keys** + +4. **Avoid duplicating the same text in multiple places** + When a translation is needed, try a search for the translation value first and see if it exists elsewhere + +5. **If a key is used across multiple bundles, it must live in ChillMainBundle.** + +6. **If a key is used across multiple bundles and is a generic term, it must be placed in the `common` namespace.** + +## Key Structure + +We use the following structure: + +``` +... +``` + +Where: + +- `` identifies the bundle or shared context +- `` identifies the part of the module using the translation +- `` describes the text purpose +- `` for a multi-level element (e.g., activity.export.person.count.description) + +### Examples of scopes + +- `activity` — ChillActivityBundle +- `person` — ChillPersonBundle +- `common` — neutral shared translation values + +## Naming Scopes + +### 1. Bundle-specific keys + +For most things inside a bundle: + +``` +activity.. +``` + +Example: + +``` +activity.form.save +activity.list.title +activity.entity.type +activity.menu.activities +activity.controller.success_created +``` + +### 2. Shared UI elements (buttons, labels, generic text) + +These belong in the `common` namespace in ChillMainBundle: + +``` +common.save +common.delete +common.edit +common.filter +common.duration_time +``` + +## Translation Workflow + +Use the following workflow when deciding where a key belongs: + +1. **Is this text used in more than one bundle?** + → Place in `main` or `common` + +2. **Is this text generic UI (button, label, pagination, yes/no)?** + → Place in `common` + +3. **Is this text specific to one bundle and one feature?** + → Place in `.feature.` + +4. **Is this text related to an entity or value object?** + → Place in `.entity..` + +5. **Is this text used in forms?** + → `.form.` or `.form.` + +6. **Is this text related to exports?** + → `.export..` + +7. **Is it related to filtering, searching or parameters?** + → `.filter.` or + → `.filter..` for nested filters + +## Examples Based on Translations Within ChillActivityBundle + +Below are concrete examples from `ChillActivityBundle`, refactored according to the guidelines. + +### General activity keys + +Instead of scattered keys like: + +``` +Show the activity +Edit the activity +Activity +Duration time +... +``` + +We use: + +``` +activity.general.show +activity.general.edit +activity.general.title +activity.general.duration +activity.general.travel_time +activity.general.attendee +activity.general.remark +activity.general.no_comments +``` + +### Forms + +Instead of keys like: + +``` +Activity creation +Save activity +Reset form +Choose a type +``` + +Use: + +``` +activity.form.title_create +activity.form.save +activity.form.reset +activity.form.choose_type +activity.form.choose_duration +``` + +Long lists (like durations) should be grouped: + +``` +activity.form.duration.5min +activity.form.duration.10min +activity.form.duration.15min +activity.form.duration.1h +activity.form.duration.1h30 +activity.form.duration.2h +... +``` + +### Entities + +Entity fields should follow: + +``` +activity.entity.activity.date +activity.entity.activity.comment +activity.entity.activity.deleted +activity.entity.location.name +activity.entity.location.type +``` + +### Controller messages + +Instead of strings as keys: + +``` +'Success : activity created!' +'The form is not valid. The activity has not been created !' +``` + +Use: + +``` +activity.controller.success_created +activity.controller.error_invalid_create +activity.controller.success_updated +activity.controller.error_invalid_update +``` + +### Roles + +Access control keys should be: + +``` +activity.role.create +activity.role.update +activity.role.see +activity.role.see_details +activity.role.delete +activity.role.stats +activity.role.list +``` + +### Admin + +``` +activity.admin.configuration +activity.admin.types +activity.admin.reasons +activity.admin.reason_category +activity.admin.presence +``` + +### CRUD + +``` +activity.crud.type.title_new +activity.crud.type.title_edit +activity.crud.presence.title_new +``` + +### Activity Reason + +``` +activity.reason.list +activity.reason.create +activity.reason.active +activity.reason.category +activity.reason.entity_title +``` + +### Exports + +Group them logically: + +``` +activity.export.person.count.title +activity.export.person.count.description +activity.export.person.count.header + +activity.export.period.sum_duration.title +activity.export.period.sum_duration.description +activity.export.period.sum_duration.header +``` + +### Filters + +Use hierarchical filters: + +``` +activity.filter.by_reason +activity.filter.by_type +activity.filter.by_date +activity.filter.by_location +activity.filter.by_sent_received +activity.filter.by_user +``` + +### Aggregators + +``` +activity.aggregator.reason.by_category +activity.aggregator.reason.level +activity.aggregator.user.by_scope +activity.aggregator.user.by_job +``` + +## Global/Shared Keys + +Keys like the following **must not be redeclared** in each bundle: + +- First name +- Last name +- Username +- ID +- Type +- Duration +- Comment +- Date +- Location +- Present / Not present +- Add / Edit / Delete / Save / Update + +These belong in `common` or `main`: + +``` +common.firstname +common.lastname +common.username +common.id +common.type +common.comment +common.date +common.location +common.present +common.absent +common.add +common.edit +common.delete +common.save +common.update +``` + +## Naming Directives Summary + +- **snake_case** +- **namespaced with dots** +- **bundle prefix for bundle-specific concepts** +- **common or main for shared concepts** +- **avoid free-floating keys (without namespace)** +- **reuse common keys wherever possible** + +## Migration Strategy (Optional) + +To apply this structure progressively: + +1. New keys must follow these guidelines. +2. Existing keys may remain as-is until refactored. +3. When refactoring: + - Move cross-bundle keys to ChillMainBundle and possible `common` namespace. + - Replace duplicated keys with shared ones. + +--- + +# Avoiding Duplicate Translations + +## 1. Use Shared Namespaces + +Two namespaces must be used for shared translations: + +- `common.*` — generic UI concepts (save, delete, date, name, etc.) + +If a translation may be reused in multiple bundles, it must be placed in the `common` namespace or in ChillMainBundle. + +## 2. Bundle-Specific Keys + +Keys belonging only to one bundle or one feature are namespaced inside that bundle: + +``` +activity.. +person.. +``` + +## 3. Search Before Creating + +Before adding a new translation key, developers must: + +1. For common translations like "enregistrer/opslaan" look in the `common` namespace. +2. Search in Loco or translations for existing values. + +If a suitable key exists, reuse it. + +## 4. Only Create a New Key When Necessary + +Create a new key only when the text is: + +- specific to the bundle +- specific to the feature +- not reusable elsewhere + +## 5. Progressive Cleanup + +Old duplicates may remain temporarily. When updating code in an area, clean duplicate values by moving them into `common` or `main`. + +## General Workflow + +- **Reuse shared keys** within `common` namespace. +- **Search before creating** new keys. +- **Namespace bundle-specific keys** under their bundle. +- **Refactor progressively** when touching old code. diff --git a/docs/source/development/translation_directives.rst b/docs/source/development/translation_directives.rst deleted file mode 100644 index 106ffb3ab..000000000 --- a/docs/source/development/translation_directives.rst +++ /dev/null @@ -1,419 +0,0 @@ -============================================ -Directives for creating new translation keys -============================================ - -These directives are meant to ensure better consistency across bundles, avoid duplication, and make keys more predictable. - - -General Principles -================== - -1. **Use lowercase snake_case for all keys** - -2. **Use dot-separated namespaces** - The dot is used to reflect: - - bundle - - feature - - sub-feature - - key type - -3. **Do not use spaces in keys** - -4. **Avoid duplicating the same text in multiple places** - When a translation is needed, try a search for the translation value first and see if it exists elsewhere - -5. **If a key is used across multiple bundles, it must live in ChillMainBundle.** - -6. **If a key is used across multiple bundles and is a generic term, it must be placed in the ``common`` namespace.** - - -Key Structure -============= - -We use the following structure: - -.. code-block:: text - - ... - -Where: - -* ``<>`` identifies the bundle or shared context -* ```` identifies the part of the module using the translation -* ```` describes the text purpose -* ```` for a multi-level element ( eg. activity.export.person.count.description) - -Examples of scopes ------------------- - -* ``activity`` — ChillActivityBundle -* ``person`` — ChillPersonBundle -* ``common`` — neutral shared translation values - - -Naming Scopes -============= - -1. **Bundle-specific keys** - - For most things inside a bundle: - - .. code-block:: text - - activity.. - - Example: - - .. code-block:: text - - activity.form.save - activity.list.title - activity.entity.type - activity.menu.activities - activity.controller.success_created - -2. **Shared UI elements (buttons, labels, generic text)** - - These belong in the ``common`` namespace in ChillMainBundle: - - .. code-block:: text - - common.save - common.delete - common.edit - common.filter - common.duration_time - -Translation workflow -==================== - -Use the following workflow when deciding where a key belongs: - -1. **Is this text used in more than one bundle?** - → Place in ``main`` or ``common`` - -2. **Is this text generic UI (button, label, pagination, yes/no)?** - → Place in ``common`` - -3. **Is this text specific to one bundle and one feature?** - → Place in ``.feature.`` - -4. **Is this text related to an entity or value object?** - → Place in ``.entity..`` - -5. **Is this text used in forms?** - → ``.form.`` or ``.form.`` - -6. **Is this text related to exports?** - → ``.export..`` - -7. **Is it related to filtering, searching or parameters?** - → ``.filter.`` or - → ``.filter..`` for nested filters - - -Examples based on translations within ChillActivityBundle -========================================================= - -Below are concrete examples from ``ChillActivityBundle``, -refactored according to the guidelines. - - -General activity keys ---------------------- - -Instead of scattered keys like:: - - Show the activity - Edit the activity - Activity - Duration time - ... - -We use: - -.. code-block:: text - - activity.general.show - activity.general.edit - activity.general.title - activity.general.duration - activity.general.travel_time - activity.general.attendee - activity.general.remark - activity.general.no_comments - - -Forms ------ - -Instead of keys like:: - - Activity creation - Save activity - Reset form - Choose a type - -Use: - -.. code-block:: text - - activity.form.title_create - activity.form.save - activity.form.reset - activity.form.choose_type - activity.form.choose_duration - -Long lists (like durations) should be grouped: - -.. code-block:: text - - activity.form.duration.5min - activity.form.duration.10min - activity.form.duration.15min - activity.form.duration.1h - activity.form.duration.1h30 - activity.form.duration.2h - ... - -Entities --------- - -Entity fields should follow: - -.. code-block:: text - - activity.entity.activity.date - activity.entity.activity.comment - activity.entity.activity.deleted - activity.entity.location.name - activity.entity.location.type - - -Controller messages -------------------- - -Instead of strings as keys:: - - 'Success : activity created!' - 'The form is not valid. The activity has not been created !' - -Use: - -.. code-block:: text - - activity.controller.success_created - activity.controller.error_invalid_create - activity.controller.success_updated - activity.controller.error_invalid_update - - -Roles ------ - -Access control keys should be: - -.. code-block:: text - - activity.role.create - activity.role.update - activity.role.see - activity.role.see_details - activity.role.delete - activity.role.stats - activity.role.list - - -Admin ------ - -.. code-block:: text - - activity.admin.configuration - activity.admin.types - activity.admin.reasons - activity.admin.reason_category - activity.admin.presence - - -CRUD ----- - -.. code-block:: text - - activity.crud.type.title_new - activity.crud.type.title_edit - activity.crud.presence.title_new - - -Activity Reason ---------------- - -.. code-block:: text - - activity.reason.list - activity.reason.create - activity.reason.active - activity.reason.category - activity.reason.entity_title - - -Exports -------- - -Group them logically: - -.. code-block:: text - - activity.export.person.count.title - activity.export.person.count.description - activity.export.person.count.header - - activity.export.period.sum_duration.title - activity.export.period.sum_duration.description - activity.export.period.sum_duration.header - - -Filters -------- - -Use hierarchical filters: - -.. code-block:: text - - activity.filter.by_reason - activity.filter.by_type - activity.filter.by_date - activity.filter.by_location - activity.filter.by_sent_received - activity.filter.by_user - - -Aggregators ------------ - -.. code-block:: text - - activity.aggregator.reason.by_category - activity.aggregator.reason.level - activity.aggregator.user.by_scope - activity.aggregator.user.by_job - - -Global/Shared Keys -================== - -Keys like the following **must not be redeclared** in each bundle: - -- First name -- Last name -- Username -- ID -- Type -- Duration -- Comment -- Date -- Location -- Present / Not present -- Add / Edit / Delete / Save / Update - -These belong in ``common`` or ``main``: - -.. code-block:: text - - common.firstname - common.lastname - common.username - common.id - common.type - common.comment - common.date - common.location - common.present - common.absent - common.add - common.edit - common.delete - common.save - common.update - - -Naming directives summary -========================== - -* **snake_case** -* **namespaced with dots** -* **bundle prefix for bundle-specific concepts** -* **common or main for shared concepts** -* **avoid free-floating keys (without namespace)** -* **reuse common keys wherever possible** - - -Migration Strategy (Optional) -============================= - -To apply this structure progressively: - -1. New keys must follow these guidelines. -2. Existing keys may remain as-is until refactored. -3. When refactoring: - - Move cross-bundle keys to ChillMainBundle and possible `common` namespace. - - Replace duplicated keys with shared ones. - -=========================================== -Avoiding duplicate translations -=========================================== - -1. Use Shared Namespaces -======================== - -Two namespaces must be used for shared translations: - -* ``common.*`` — generic UI concepts (save, delete, date, name, etc.) - -If a translation may be reused in multiple bundles, it must be placed -in the ``common`` namespace or in ChillMainBundle. - -2. Bundle-Specific Keys -======================= - -Keys belonging only to one bundle or one feature are namespaced inside that -bundle: - -.. code-block:: text - - activity.. - person.. - -3. Search Before Creating -========================= - -Before adding a new translation key, developers must: - -1. For common translations like: "enregistrer/opslaan" look in the `common` namespace. -3. Search in Loco or translations for existing values. - -If a suitable key exists, reuse it. - -4. Only Create a New Key When Necessary -======================================= - -Create a new key only when the text is: - -* specific to the bundle -* specific to the feature -* not reusable elsewhere - -6. Progressive Cleanup -====================== - -Old duplicates may remain temporarily. When updating code in an area, clean -duplicate values by moving them into ``common`` or ``main``. - -General workflow -================ - -* **Reuse shared keys** within ``common`` namespace. -* **Search before creating** new keys. -* **Namespace bundle-specific keys** under their bundle. -* **Refactor progressively** when touching old code. diff --git a/docs/source/development/translation_provider.md b/docs/source/development/translation_provider.md new file mode 100644 index 000000000..29301f366 --- /dev/null +++ b/docs/source/development/translation_provider.md @@ -0,0 +1,139 @@ +# Managing Translations Within CHILL Using Loco as a Translation Provider + +Within CHILL we make use of Symfony's translation component together with *Loco* as an external translation provider. Using this setup centralises translations in a single online location (Loco), while still allowing developers to create and update translation keys locally in the project (YAML files). + +## Workflow + +We use the following workflow: + +- Developers create translation keys in YAML files inside each bundle. +- Keys are written in **English**. +- Application UI defaults to **French**, with **Dutch** as an additional locale (other languages can be added in the future). +- Loco acts as the central translation memory and synchronisation source. +- Loco Symfony package was installed so that built-in translation commands can be used to push/pull content between Loco and the local project. + +## Translation Directory Structure + +Each bundle contains its own `translations` directory, for example: + +``` +chill-bundles/ + ChillCoreBundle/ + translations/ + messages.fr.yml + messages.nl.yml + ChillPersonBundle/ + translations/ + messages.fr.yml + messages.nl.yml + ... +``` + +## Configuration + +The translation configuration is defined in `config/packages/translation.yaml`: + +```yaml +framework: + default_locale: '%env(resolve:LOCALE)%' + translator: + default_path: '%kernel.project_dir%/translations' + fallbacks: + - '%env(resolve:LOCALE)%' + - 'en' + providers: + loco: + dsn: '%env(LOCO_DSN)%' + domains: [ 'messages' ] + locales: [ 'fr', 'nl' ] +``` + +Note: + +- `en` is the **source locale** in Loco. +- `fr` and `nl` are the **application locales**. +- `domains: [messages]` means only `messages.*.yml` files are pushed. + +### Environment Variables + +In `.env`: + +``` +LOCALE=fr +``` + +In `.env.local`: + +``` +LOCO_DSN="loco://API_KEY@default" +``` + +Replace `API_KEY` with the key provided by Loco. + +## Working with Loco + +Loco shows all translation keys under three languages: + +- **English (source)** — keys are listed but remain "untranslated" +- **French** — translated strings for French users +- **Dutch** — translated strings for Dutch users + +Note: Don't add translations directly in the English column. This column simply represents the *key*. + +## Pushing Translations to Loco + +You can push local translations to Loco using: + +```bash +symfony console translation:push loco --locales=fr --locales=nl --force +``` + +This will: + +- Upload all French and Dutch translation values from `*.fr.yml` and `*.nl.yml` files +- Ensure Loco stays in sync with local YAML files +- Create any missing keys in Loco + +## Pulling Translations from Loco + +When translators update strings in Loco, developers can fetch updates with: + +```bash +symfony console translation:pull loco --locales=fr --locales=nl --force +``` + +This will: + +- Download the latest French and Dutch translations +- Overwrite the local YAML files with Loco's content +- Keep everything consistent across the team + +## Adding New Translation Keys (Developer Workflow) + +1. Add a new key directly in the appropriate YAML file, for example: + + ``` + chill-bundles/ChillPersonBundle/translations/messages.fr.yml + ``` + + Example key: + + ```yaml + person.form.submit: "Envoyer" + ``` + +2. Add Dutch translation as well if you can (otherwise leave empty to be translated within Loco later): + + ```yaml + person.form.submit: "Verzenden" + ``` + +3. Run a push to send the new key to Loco: + + ```bash + symfony console translation:push loco --locales=fr --locales=nl --force + ``` + +4. The key will now appear in Loco for translation management. + +Note: English appears as "untranslated", because it is merely the source language. diff --git a/docs/source/development/translation_provider.rst b/docs/source/development/translation_provider.rst deleted file mode 100644 index 98b117129..000000000 --- a/docs/source/development/translation_provider.rst +++ /dev/null @@ -1,148 +0,0 @@ -======================================================================= -Managing translations within CHILL using Loco as a translation provider -======================================================================= - -Within CHILL we make use of Symfony's translation component together with *Loco* as an external -translation provider. Using this setup centralise translations in a single online -location (Loco), while still allowing developers to create and update -translation keys locally in the project (YAML files). - -Workflow -======== - -We use the following workflow: - -* Developers create translation keys in YAML files inside each bundle. -* Keys are written in **English**. -* Application UI defaults to **French**, with **Dutch** as an additional locale (other languages can be added in the future). -* Loco acts as the central translation memory and synchronisation source. -* Loco Symfony package was installed so that built-in translation commands can be used to push/pull content - between Loco and the local project. - - -Translation directory structure -=============================== - -Each bundle contains its own ``translations`` directory, for example:: - - chill-bundles/ - ChillCoreBundle/ - translations/ - messages.fr.yml - messages.nl.yml - ChillPersonBundle/ - translations/ - messages.fr.yml - messages.nl.yml - ... - -Configuration -============= - -The translation configuration is defined in -``config/packages/translation.yaml``:: - - framework: - default_locale: '%env(resolve:LOCALE)%' - translator: - default_path: '%kernel.project_dir%/translations' - fallbacks: - - '%env(resolve:LOCALE)%' - - 'en' - providers: - loco: - dsn: '%env(LOCO_DSN)%' - domains: [ 'messages' ] - locales: [ 'fr', 'nl' ] - -Note: - -* ``en`` is the **source locale** in Loco. -* ``fr`` and ``nl`` are the **application locales**. -* ``domains: [messages]`` means only ``messages.*.yml`` files are pushed. - - -Environment variables ---------------------- - -In ``.env``:: - - LOCALE=fr - -In ``.env.local``:: - - LOCO_DSN="loco://API_KEY@default" - -Replace ``API_KEY`` with the key provided by Loco. - - -Working with Loco -================= - -Loco shows all translation keys under three languages: - -* **English (source)** — keys are listed but remain “untranslated” -* **French** — translated strings for French users -* **Dutch** — translated strings for Dutch users - -Note: Don't add translations directly in the English column. -This column simply represents the *key*. - - -Pushing translations to Loco -============================ - -You can push local translations to Loco using: - -.. code-block:: bash - - symfony console translation:push loco --locales=fr --locales=nl --force - -This will: - -* Upload all French and Dutch translation values from ``*.fr.yml`` and - ``*.nl.yml`` files -* Ensures Loco stays in sync with local YAML files -* Creates any missing keys in Loco - - -Pulling translations from Loco -============================== - -When translators update strings in Loco, developers can fetch updates with: - -.. code-block:: bash - - symfony console translation:pull loco --locales=fr --locales=nl --force - -This will: - -* Download the latest French and Dutch translations -* Overwrite the local YAML files with Loco’s content -* Keep everything consistent across the team - - -Adding new translation keys (Developer workflow) -================================================ - -1. Add a new key directly in the appropriate YAML file, for example:: - - chill-bundles/ChillPersonBundle/translations/messages.fr.yml - - Example key:: - - person.form.submit: "Envoyer" - -2. Add Dutch translation as well if you can (otherwise leave empty to be translated within Loco later):: - - person.form.submit: "Verzenden" - -3. Run a push to send the new key to Loco: - -.. code-block:: bash - - symfony console translation:push loco --locales=fr --locales=nl --force - -4. The key will now appear in Loco for translation management. - -Note: English appears as “untranslated”, because it is merely the source language diff --git a/packages/ChillZimbraBundle/composer.json b/packages/ChillZimbraBundle/composer.json index f62e21125..4a53b3ed5 100644 --- a/packages/ChillZimbraBundle/composer.json +++ b/packages/ChillZimbraBundle/composer.json @@ -9,7 +9,7 @@ "social worker" ], "require": { - "chill-project/chill-bundles": "dev-master as v4.6.1", + "chill-project/chill-bundles": "^4.9.0", "zimbra-api/soap-api": "^3.2.2", "psr/http-client": "^1.0", "nyholm/psr7": "^1.0" diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 38c3f7b61..60c36771e 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -3,7 +3,6 @@ parameters: paths: - src/ - utils/ - - packages/ tmpDir: var/cache/phpstan reportUnmatchedIgnoredErrors: false excludePaths: diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php index e304a9d7f..b901b10f5 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php @@ -27,7 +27,8 @@ class ByActivityNumberAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb - ->addSelect('(SELECT COUNT(activity.id) FROM '.Activity::class.' activity WHERE activity.accompanyingPeriod = acp) AS activity_by_number_aggregator') + // Use a distinct alias inside the subquery to avoid colliding with the root alias "activity" + ->addSelect('(SELECT COUNT(agg_activity.id) FROM '.Activity::class.' agg_activity WHERE agg_activity.accompanyingPeriod = acp) AS activity_by_number_aggregator') ->addGroupBy('activity_by_number_aggregator'); } @@ -65,7 +66,7 @@ class ByActivityNumberAggregator implements AggregatorInterface { return static function ($value) { if ('_header' === $value) { - return ''; + return 'Count activities linked to an accompanying period'; } if (null === $value) { diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue index c7dfbf778..dff32b35d 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue @@ -1,114 +1,123 @@ diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index af9135177..fd2574a41 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -43,6 +43,7 @@ use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\Order; use Doctrine\Common\Collections\ReadableCollection; use Doctrine\Common\Collections\Selectable; use Doctrine\ORM\Mapping as ORM; @@ -140,6 +141,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI #[ORM\ManyToMany(targetEntity: Calendar::class, mappedBy: 'persons')] private Collection $calendars; + /** + * @var Collection&Selectable + */ + #[ORM\OneToMany(mappedBy: 'person', targetEntity: Calendar::class)] + private Collection $directCalendars; + /** * The person's center. * @@ -409,6 +416,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI public function __construct() { $this->calendars = new ArrayCollection(); + $this->directCalendars = new ArrayCollection(); $this->accompanyingPeriodParticipations = new ArrayCollection(); $this->spokenLanguages = new ArrayCollection(); $this->addresses = new ArrayCollection(); @@ -869,6 +877,30 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI return $this->calendars; } + /** + * Get next calendars for this person (calendars with start date after today). + * Only returns calendars that are directly linked to this person via the person property, + * not those linked through AccompanyingPeriods. + * + * @param int|null $limit Optional limit for the number of results + * + * @return array + */ + public function getNextCalendarsForPerson(?int $limit = null): array + { + $today = new \DateTimeImmutable('today'); + + $criteria = Criteria::create() + ->where(Criteria::expr()->gte('startDate', $today)) + ->orderBy(['startDate' => Order::Ascending]); + + if (null !== $limit) { + $criteria->setMaxResults($limit); + } + + return $this->directCalendars->matching($criteria)->toArray(); + } + public function getCenter(): ?Center { if (null !== $this->centerCurrent) { @@ -1122,7 +1154,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI ->where( $expr->eq('shareHousehold', false) ) - ->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Descending]); + ->orderBy(['startDate' => Order::Descending]); return $this->getHouseholdParticipations() ->matching($criteria); @@ -1144,7 +1176,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI ->where( $expr->eq('shareHousehold', true) ) - ->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Descending, 'id' => \Doctrine\Common\Collections\Order::Descending]); + ->orderBy(['startDate' => Order::Descending, 'id' => Order::Descending]); return $this->getHouseholdParticipations() ->matching($criteria); diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig index 5572bd7b1..c373fd58b 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig @@ -212,4 +212,3 @@ {%- endif -%} - diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig index 0840cecb6..cf7fb1466 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig @@ -301,6 +301,47 @@ 'customButtons': { 'after': _self.button_person_after(person), 'before': _self.button_person_before((person)) } }) }} + {% set calendars = [] %} + {% for c in person.getNextCalendarsForPerson(10) %} + {% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', c) %} + {% set calendars = calendars|merge([c]) %} + {% endif %} + {% endfor %} + + {% if calendars|length > 0 %} +
+
+
+

{{ 'chill_calendar.Next calendars'|trans }}

+
+
+
+ + {% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', person) %} + + {% endif %} +
+
+
+
+ {% endif %} + {#- 'acps' is for AcCompanyingPeriodS #} {%- set acps = [] %} {%- set acpsClosed = [] %} diff --git a/src/Bundle/ChillWopiBundle/chill.webpack.config.js b/src/Bundle/ChillWopiBundle/chill.webpack.config.js index 05c231f68..8580597cc 100644 --- a/src/Bundle/ChillWopiBundle/chill.webpack.config.js +++ b/src/Bundle/ChillWopiBundle/chill.webpack.config.js @@ -2,7 +2,7 @@ module.exports = function (encore, entries) { encore.addEntry( "page_wopi_editor", - __dirname + "/src/Resources/public/page/editor/index.js", + __dirname + "/src/Resources/public/page/editor/index.ts", ); encore.addEntry( "mod_reload_page", diff --git a/src/Bundle/ChillWopiBundle/src/Resources/public/page/editor/index.js b/src/Bundle/ChillWopiBundle/src/Resources/public/page/editor/index.js deleted file mode 100644 index d946977df..000000000 --- a/src/Bundle/ChillWopiBundle/src/Resources/public/page/editor/index.js +++ /dev/null @@ -1,46 +0,0 @@ -require("./index.scss"); - -window.addEventListener("DOMContentLoaded", function () { - let frameholder = document.getElementById("frameholder"); - let office_frame = document.createElement("iframe"); - office_frame.name = "office_frame"; - office_frame.id = "office_frame"; - - // The title should be set for accessibility - office_frame.title = "Office Frame"; - - // This attribute allows true fullscreen mode in slideshow view - // when using PowerPoint's 'view' action. - office_frame.setAttribute("allowfullscreen", "true"); - - // The sandbox attribute is needed to allow automatic redirection to the O365 sign-in page in the business user flow - office_frame.setAttribute( - "sandbox", - "allow-downloads allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-top-navigation allow-popups-to-escape-sandbox", - ); - frameholder.appendChild(office_frame); - - document.getElementById("office_form").submit(); - - const url = new URL(editor_url); - const editor_domain = url.origin; - - window.addEventListener("message", function (message) { - if (message.origin !== editor_domain) { - return; - } - - let data = JSON.parse(message.data); - - if ("UI_Close" === data.MessageId) { - closeEditor(); - } - }); -}); - -function closeEditor() { - let params = new URLSearchParams(window.location.search), - returnPath = params.get("returnPath"); - - window.location.assign(returnPath); -} diff --git a/src/Bundle/ChillWopiBundle/src/Resources/public/page/editor/index.ts b/src/Bundle/ChillWopiBundle/src/Resources/public/page/editor/index.ts new file mode 100644 index 000000000..01ddad995 --- /dev/null +++ b/src/Bundle/ChillWopiBundle/src/Resources/public/page/editor/index.ts @@ -0,0 +1,68 @@ +import "./index.scss"; + +// Provided by the server-side template +declare const editor_url: string; + +window.addEventListener("DOMContentLoaded", function () { + const frameholder = document.getElementById("frameholder"); + const office_frame = document.createElement("iframe"); + office_frame.name = "office_frame"; + office_frame.id = "office_frame"; + + // The title should be set for accessibility + office_frame.title = "Office Frame"; + + // This attribute allows true fullscreen mode in slideshow view + // when using PowerPoint's 'view' action. + office_frame.setAttribute("allowfullscreen", "true"); + + // The sandbox attribute is needed to allow automatic redirection to the O365 sign-in page in the business user flow + office_frame.setAttribute( + "sandbox", + "allow-downloads allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-top-navigation allow-popups-to-escape-sandbox", + ); + + office_frame.setAttribute( + "allow", + "clipboard-read *; clipboard-write *; fullscreen *", + ); + if (frameholder) { + frameholder.appendChild(office_frame); + } + + const officeForm = document.getElementById( + "office_form", + ) as HTMLFormElement | null; + officeForm?.submit(); + + const url = new URL(editor_url); + const editor_domain = url.origin; + + window.addEventListener("message", function (message: MessageEvent) { + if (message.origin !== editor_domain) { + return; + } + + let data: { MessageId: "UI_Close" | null; data: string }; + try { + data = + typeof message.data === "string" + ? JSON.parse(message.data) + : message.data; + } catch (e: unknown) { + console.error("error while parsing data from message UI_CLOSE", e); + return; + } + + if ("UI_Close" === data.MessageId) { + closeEditor(); + } + }); +}); + +function closeEditor(): void { + const params = new URLSearchParams(window.location.search); + const returnPath = params.get("returnPath") ?? "/"; + + window.location.assign(returnPath); +} diff --git a/symfony.lock b/symfony.lock index 5724d23b9..532c9ec98 100644 --- a/symfony.lock +++ b/symfony.lock @@ -2,9 +2,6 @@ "champs-libres/wopi-bundle": { "version": "dev-master" }, - "chill-project/chill-zimbra-bundle": { - "version": "dev-472-zimbra-connector" - }, "doctrine/annotations": { "version": "1.14", "recipe": {