mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-12-12 13:23:15 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ba4e445110
|
|||
|
0f1ff9baf4
|
|||
| e4365ad058 | |||
| a16d659f69 | |||
| c4a069ba2e | |||
| 2600c6fa2a | |||
| 09ef95d13e | |||
| c19b2e18ad | |||
| 27853c594d | |||
| 7714b07a9d | |||
|
6e1c9b6f29
|
|||
|
d80661e479
|
|||
|
473c5a9fa3
|
|||
| acceeeaa2a | |||
| 53b02a0ced | |||
| 387bf55b11 |
6
.changes/v4.10.0.md
Normal file
6
.changes/v4.10.0.md
Normal file
@@ -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
|
||||||
|
|
||||||
6
.changes/v4.10.1.md
Normal file
6
.changes/v4.10.1.md
Normal file
@@ -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
|
||||||
14
CHANGELOG.md
14
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).
|
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
|
## v4.9.0 - 2025-12-05
|
||||||
### Feature
|
### Feature
|
||||||
* ([#459](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/459)) Add a counter for invitations awaiting reply
|
* ([#459](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/459)) Add a counter for invitations awaiting reply
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"ext-redis": "*",
|
"ext-redis": "*",
|
||||||
"ext-zlib": "*",
|
"ext-zlib": "*",
|
||||||
"chill-project/chill-zimbra-bundle": "@dev",
|
|
||||||
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
||||||
"champs-libres/wopi-lib": "dev-master@dev",
|
"champs-libres/wopi-lib": "dev-master@dev",
|
||||||
"doctrine/data-fixtures": "^1.8",
|
"doctrine/data-fixtures": "^1.8",
|
||||||
@@ -62,7 +61,6 @@
|
|||||||
"symfony/http-client": "^5.4",
|
"symfony/http-client": "^5.4",
|
||||||
"symfony/http-foundation": "^5.4",
|
"symfony/http-foundation": "^5.4",
|
||||||
"symfony/intl": "^5.4",
|
"symfony/intl": "^5.4",
|
||||||
"symfony/loco-translation-provider": "^6.0",
|
|
||||||
"symfony/mailer": "^5.4",
|
"symfony/mailer": "^5.4",
|
||||||
"symfony/messenger": "^5.4",
|
"symfony/messenger": "^5.4",
|
||||||
"symfony/mime": "^5.4",
|
"symfony/mime": "^5.4",
|
||||||
@@ -119,7 +117,8 @@
|
|||||||
"symfony/runtime": "^5.4",
|
"symfony/runtime": "^5.4",
|
||||||
"symfony/stopwatch": "^5.4",
|
"symfony/stopwatch": "^5.4",
|
||||||
"symfony/var-dumper": "^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": {
|
"conflict": {
|
||||||
"symfony/symfony": "*"
|
"symfony/symfony": "*"
|
||||||
|
|||||||
@@ -37,5 +37,4 @@ return [
|
|||||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
||||||
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
|
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
|
||||||
Chill\ZimbraBundle\ChillZimbraBundle::class => ['all' => true],
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,3 +11,6 @@ services:
|
|||||||
autowire: true # Automatically injects dependencies in your services.
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||||
|
|
||||||
|
when@dev:
|
||||||
|
services:
|
||||||
|
ChampsLibres\WopiLib\Contract\Service\ProofValidatorInterface: '@Chill\WopiBundle\Service\Wopi\NullProofValidator'
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ As Chill relies on the [symfony ](http://symfony.com) framework, reading the fra
|
|||||||
- [Messages to users](messages-to-users.md)
|
- [Messages to users](messages-to-users.md)
|
||||||
- [Pagination](pagination.md)
|
- [Pagination](pagination.md)
|
||||||
- [Localisation](localisation.md)
|
- [Localisation](localisation.md)
|
||||||
|
- [Translation directives](translation_directives.md)
|
||||||
|
- [Translation provider](translation_provider.md)
|
||||||
- [Logging](logging.md)
|
- [Logging](logging.md)
|
||||||
- [Database migrations](migrations.md)
|
- [Database migrations](migrations.md)
|
||||||
- [Searching](searching.md)
|
- [Searching](searching.md)
|
||||||
|
|||||||
376
docs/source/development/translation_directives.md
Normal file
376
docs/source/development/translation_directives.md
Normal file
@@ -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:
|
||||||
|
|
||||||
|
```
|
||||||
|
<scope>.<feature>.<sub-feature>.<key-type>
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
|
||||||
|
- `<scope>` identifies the bundle or shared context
|
||||||
|
- `<feature>` identifies the part of the module using the translation
|
||||||
|
- `<element>` describes the text purpose
|
||||||
|
- `<subelement>` 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.<feature>.<element>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 `<bundle>.feature.<element>`
|
||||||
|
|
||||||
|
4. **Is this text related to an entity or value object?**
|
||||||
|
→ Place in `<bundle>.entity.<entityname>.<field>`
|
||||||
|
|
||||||
|
5. **Is this text used in forms?**
|
||||||
|
→ `<bundle>.form.<field>` or `<bundle>.form.<action>`
|
||||||
|
|
||||||
|
6. **Is this text related to exports?**
|
||||||
|
→ `<bundle>.export.<feature>.<column>`
|
||||||
|
|
||||||
|
7. **Is it related to filtering, searching or parameters?**
|
||||||
|
→ `<bundle>.filter.<name>` or
|
||||||
|
→ `<bundle>.filter.<feature>.<field>` 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.<feature>.<element>
|
||||||
|
person.<feature>.<element>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -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
|
|
||||||
|
|
||||||
<scope>.<feature>.<sub-feature>.<key-type>
|
|
||||||
|
|
||||||
Where:
|
|
||||||
|
|
||||||
* ``<>`` identifies the bundle or shared context
|
|
||||||
* ``<feature>`` identifies the part of the module using the translation
|
|
||||||
* ``<element>`` describes the text purpose
|
|
||||||
* ``<subelement>`` 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.<feature>.<element>
|
|
||||||
|
|
||||||
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 ``<bundle>.feature.<element>``
|
|
||||||
|
|
||||||
4. **Is this text related to an entity or value object?**
|
|
||||||
→ Place in ``<bundle>.entity.<entityname>.<field>``
|
|
||||||
|
|
||||||
5. **Is this text used in forms?**
|
|
||||||
→ ``<bundle>.form.<field>`` or ``<bundle>.form.<action>``
|
|
||||||
|
|
||||||
6. **Is this text related to exports?**
|
|
||||||
→ ``<bundle>.export.<feature>.<column>``
|
|
||||||
|
|
||||||
7. **Is it related to filtering, searching or parameters?**
|
|
||||||
→ ``<bundle>.filter.<name>`` or
|
|
||||||
→ ``<bundle>.filter.<feature>.<field>`` 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.<feature>.<element>
|
|
||||||
person.<feature>.<element>
|
|
||||||
|
|
||||||
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.
|
|
||||||
139
docs/source/development/translation_provider.md
Normal file
139
docs/source/development/translation_provider.md
Normal file
@@ -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.
|
||||||
@@ -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
|
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
"@hotwired/stimulus": "^3.0.0",
|
"@hotwired/stimulus": "^3.0.0",
|
||||||
"@luminateone/eslint-baseline": "^1.0.9",
|
"@luminateone/eslint-baseline": "^1.0.9",
|
||||||
"@symfony/stimulus-bridge": "^3.2.0",
|
"@symfony/stimulus-bridge": "^3.2.0",
|
||||||
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
|
|
||||||
"@symfony/webpack-encore": "^4.1.0",
|
"@symfony/webpack-encore": "^4.1.0",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"social worker"
|
"social worker"
|
||||||
],
|
],
|
||||||
"require": {
|
"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",
|
"zimbra-api/soap-api": "^3.2.2",
|
||||||
"psr/http-client": "^1.0",
|
"psr/http-client": "^1.0",
|
||||||
"nyholm/psr7": "^1.0"
|
"nyholm/psr7": "^1.0"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ parameters:
|
|||||||
paths:
|
paths:
|
||||||
- src/
|
- src/
|
||||||
- utils/
|
- utils/
|
||||||
- packages/
|
|
||||||
tmpDir: var/cache/phpstan
|
tmpDir: var/cache/phpstan
|
||||||
reportUnmatchedIgnoredErrors: false
|
reportUnmatchedIgnoredErrors: false
|
||||||
excludePaths:
|
excludePaths:
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class ByActivityNumberAggregator implements AggregatorInterface
|
|||||||
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
||||||
{
|
{
|
||||||
$qb
|
$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');
|
->addGroupBy('activity_by_number_aggregator');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class ByActivityNumberAggregator implements AggregatorInterface
|
|||||||
{
|
{
|
||||||
return static function ($value) {
|
return static function ($value) {
|
||||||
if ('_header' === $value) {
|
if ('_header' === $value) {
|
||||||
return '';
|
return 'Count activities linked to an accompanying period';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $value) {
|
if (null === $value) {
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ import {
|
|||||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||||
ACTIVITY_CREATE_NEW_LOCATION,
|
ACTIVITY_CREATE_NEW_LOCATION,
|
||||||
|
ACTIVITY_EDIT_ADDRESS,
|
||||||
|
ACTIVITY_CREATE_ADDRESS,
|
||||||
trans,
|
trans,
|
||||||
} from "translator";
|
} from "translator";
|
||||||
|
|
||||||
@@ -156,6 +158,8 @@ export default {
|
|||||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||||
ACTIVITY_CREATE_NEW_LOCATION,
|
ACTIVITY_CREATE_NEW_LOCATION,
|
||||||
|
ACTIVITY_EDIT_ADDRESS,
|
||||||
|
ACTIVITY_CREATE_ADDRESS,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: ["availableLocations"],
|
props: ["availableLocations"],
|
||||||
@@ -179,14 +183,14 @@ export default {
|
|||||||
options: {
|
options: {
|
||||||
button: {
|
button: {
|
||||||
text: {
|
text: {
|
||||||
create: "activity.create_address",
|
create: ACTIVITY_CREATE_ADDRESS,
|
||||||
edit: "activity.edit_address",
|
edit: ACTIVITY_EDIT_ADDRESS,
|
||||||
},
|
},
|
||||||
size: "btn-sm",
|
size: "btn-sm",
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
create: "activity.create_address",
|
create: ACTIVITY_CREATE_ADDRESS,
|
||||||
edit: "activity.edit_address",
|
edit: ACTIVITY_EDIT_ADDRESS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ use DateTime;
|
|||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\Common\Collections\Criteria;
|
use Doctrine\Common\Collections\Criteria;
|
||||||
|
use Doctrine\Common\Collections\Order;
|
||||||
use Doctrine\Common\Collections\ReadableCollection;
|
use Doctrine\Common\Collections\ReadableCollection;
|
||||||
use Doctrine\Common\Collections\Selectable;
|
use Doctrine\Common\Collections\Selectable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@@ -139,6 +140,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
#[ORM\ManyToMany(targetEntity: Calendar::class, mappedBy: 'persons')]
|
#[ORM\ManyToMany(targetEntity: Calendar::class, mappedBy: 'persons')]
|
||||||
private Collection $calendars;
|
private Collection $calendars;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Calendar>&Selectable<int, Calendar>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'person', targetEntity: Calendar::class)]
|
||||||
|
private Collection $directCalendars;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The person's center.
|
* The person's center.
|
||||||
*
|
*
|
||||||
@@ -406,6 +413,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->calendars = new ArrayCollection();
|
$this->calendars = new ArrayCollection();
|
||||||
|
$this->directCalendars = new ArrayCollection();
|
||||||
$this->accompanyingPeriodParticipations = new ArrayCollection();
|
$this->accompanyingPeriodParticipations = new ArrayCollection();
|
||||||
$this->spokenLanguages = new ArrayCollection();
|
$this->spokenLanguages = new ArrayCollection();
|
||||||
$this->addresses = new ArrayCollection();
|
$this->addresses = new ArrayCollection();
|
||||||
@@ -866,6 +874,30 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
return $this->calendars;
|
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<Calendar>
|
||||||
|
*/
|
||||||
|
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
|
public function getCenter(): ?Center
|
||||||
{
|
{
|
||||||
if (null !== $this->centerCurrent) {
|
if (null !== $this->centerCurrent) {
|
||||||
@@ -1119,7 +1151,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
->where(
|
->where(
|
||||||
$expr->eq('shareHousehold', false)
|
$expr->eq('shareHousehold', false)
|
||||||
)
|
)
|
||||||
->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Descending]);
|
->orderBy(['startDate' => Order::Descending]);
|
||||||
|
|
||||||
return $this->getHouseholdParticipations()
|
return $this->getHouseholdParticipations()
|
||||||
->matching($criteria);
|
->matching($criteria);
|
||||||
@@ -1141,7 +1173,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
->where(
|
->where(
|
||||||
$expr->eq('shareHousehold', true)
|
$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()
|
return $this->getHouseholdParticipations()
|
||||||
->matching($criteria);
|
->matching($criteria);
|
||||||
|
|||||||
@@ -212,4 +212,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
|
|||||||
@@ -301,6 +301,47 @@
|
|||||||
'customButtons': { 'after': _self.button_person_after(person), 'before': _self.button_person_before((person)) }
|
'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 %}
|
||||||
|
<div class="wrap-list periods-list">
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">
|
||||||
|
<h3>{{ 'chill_calendar.Next calendars'|trans }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
<div class="calendar-list">
|
||||||
|
<ul class="calendar-list">
|
||||||
|
{% for c in calendars %}
|
||||||
|
<li>
|
||||||
|
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', { id: c.id }) }}">
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
{{ c.startDate|format_datetime('long', 'short') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
{{ c.startDate|format_datetime('long', 'short') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', person) %}
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_list_by_person', {'id': person.id}) }}" class="calendar-list__global"><i class="fa fa-list"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{#- 'acps' is for AcCompanyingPeriodS #}
|
{#- 'acps' is for AcCompanyingPeriodS #}
|
||||||
{%- set acps = [] %}
|
{%- set acps = [] %}
|
||||||
{%- set acpsClosed = [] %}
|
{%- set acpsClosed = [] %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
module.exports = function (encore, entries) {
|
module.exports = function (encore, entries) {
|
||||||
encore.addEntry(
|
encore.addEntry(
|
||||||
"page_wopi_editor",
|
"page_wopi_editor",
|
||||||
__dirname + "/src/Resources/public/page/editor/index.js",
|
__dirname + "/src/Resources/public/page/editor/index.ts",
|
||||||
);
|
);
|
||||||
encore.addEntry(
|
encore.addEntry(
|
||||||
"mod_reload_page",
|
"mod_reload_page",
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -2,9 +2,6 @@
|
|||||||
"champs-libres/wopi-bundle": {
|
"champs-libres/wopi-bundle": {
|
||||||
"version": "dev-master"
|
"version": "dev-master"
|
||||||
},
|
},
|
||||||
"chill-project/chill-zimbra-bundle": {
|
|
||||||
"version": "dev-472-zimbra-connector"
|
|
||||||
},
|
|
||||||
"doctrine/annotations": {
|
"doctrine/annotations": {
|
||||||
"version": "1.14",
|
"version": "1.14",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
Reference in New Issue
Block a user