Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Debt] Move enum localization to backend #10701

Merged
merged 75 commits into from
Jul 5, 2024

Conversation

esizer
Copy link
Member

@esizer esizer commented Jun 19, 2024

πŸ€– Resolves #10634

πŸ‘‹ Introduction

Moves the localization of specific enums to the backend.

πŸ•΅οΈ Details

This moves most of the strings found in localized constants to the backend using a few new tools. We have a new concept of a localized enum. This takes any PHP enum in the App\Enums namespace and adds a localized string to each case (assuming it exists).

In order to create a localized enum, there are a few things you must do:

  1. Implement the HasLocalization trait on the eum (meaning defining a lang file)
  2. Create the lang file with the name (in /lang/{en|fr}.php matching what you defined in the enum.
  3. Add the strings in an associative array where the key is a lowercase version of the enum case you are localizing
  4. Add the enum to the $localizedEnums array in the GraphQLServiceProvider
  5. Mark the enum as localized in the graphql schema using the @localizedEnum directive
  6. Update the frontend to use the new Localized* type

βš’οΈ How it works

HasLocalization trait

This trait will force you to implement the getLangFile method which needs to return a string that represents the file name of the lang file.

It then exposes a static function localizedString on the enum that takes a case of the enum and will return a localized string.

Lang files

These files are found in the lang folder and then organized again by locale. They simply contain associative arrays in a similar fashion to laravel config files. You just need to define a key that matches a lowercase version of an enum case and then the value is the string we want to render.

If a key for a speciific case does not exist in the lang file, it will first attempt to fallback to English and then, if that fails, just print out the expected key (i.e citizenship_status.other). Ideally we could find a way to provide a fallback message like "Not found" πŸ€”

Common strings

Of course, we have a few enums that share a common string like "Other". Thankfully, it is fairly simple to share these strings. We have a common.php file where you can store these and then access them using the Lang facade.

Lang::get('common.key', [], 'locale');

Graphql

We register these enums as a custom type in graphql in the GraphQLServiceProvider. Since these are all identical, all you need to do is define it in the array. We then loop through them and register it. The result is the following type (where EnunName is the name of the enum):

type LocalizedEnumName {
   value: EnumName!
   label: LocalizedString!
}
Localized enum directive

Even though the type is registered, in order to actually use it, you will need to apply the @localizedEnum directive. This takes the value of the enum from the database and then converts it into the new type. If the key in graphql does not match the attribute on the PHP model, you can supply the attribute argument and it will behave in a similar way to the @rename directive.

Retrieving all strings for enum

Sometimes (usually in forms) we need to get all the strings of an enum for things like input options. In order to do this we have a simple query that just returns a basic version of the localized enum type. It has validation on the enum name which will fail if the enum does not exist in App\Enums or if it does not implement the HasLocalization trait.

type LocalizedEnumString {
  value: String!
  label: LocalizedString!
}

  localizedEnumStrings(
    enumName: String!
      @rules(apply: ["required", "App\\Rules\\LocalizedEnumExists"]) <- ensures the enum name exists and is localized
  ): [LocalizedEnumString!]
  
# usage

query {
  # it is helpful to alias these so you can tell what it is in the client code
  citizenshipStatuses: localizedEnumStrings(enumName: "CitizenshipStatus") {
     value
     label {
       en
       fr
     }
  }
}

Mocking

Since we no longer have the strings for all these enums in the client code, we need to mock them for storybook and jest. We now have a helper in the @gc-digital-talent/fake-data package called toLocalizedEnum. This is basic helper that just takes the enum and converts it to the proper tpye signature making the label just the enum value replacing _ with a space and making it lower case.

Helpers

Forms

We often use all cases in an enum for input options. We have a helper enumToOptions that converts the cases to an options array. However, this will not work with localized enums. Now, we should use the new localizedEnumToOptions helper. Since we do not have strings in the client code, you will need to query them using the new query and pass them in.

<Select
   options={localizedEnumToOptions(dataFromApi, intl)}
   ...
 />

Others

We have some other helpers found in @gc-digital-talent/i18n. I'm not sure if this is the correct location but that is where they ended up. If we want to move them somewhere else, that can be done easily.

Sometimes, we have the value of an enum but not the localized version. We have two ways to convert them. One will return the entire localized enum type and the other just returns the localized string in the current locale.

const localizedEnum = getLocalizedEnumByValue(value, dataFromApi);

const label = getLocalizedEnumStringByValue(value, dataFromApi, intl);

Also, alot of these enums need specific sorting. We have all of these sort helpers there as well as a generic sorter that just takes an array for order and the data from the API.

😠 Things I don't like

Maintaining the localized enum array

Having to maintain an array in the GraphQLServiceProvider is not great and I would like to find a way to "discover" any enum that is localized automatically. However, we do not add them that frequently so I think this could work for some time.

Fallbacks

Right now if you do not provide a string it will just print out the key for the string. I would love if we could automatically fallback to either a "not found" string or just null πŸ€”

Mocking

See the previous comment about mocking for details. tldr; We no longer have real enum strings in storybook or jest tests.

πŸ§ͺ Testing

  1. Refresh API make refresh-api
  2. Clear cache make artisan CMD="cache:clear"
  3. Build the app pnpm run dev:fresh
  4. Review pages

πŸ” Review Plan

Enum Reviewer Done
ArmedForcesStatus.php Peter
AssessmentDecisionLevel.php Peter
AssessmentDecision.php Peter
AssessmentResultJustification.php Peter
AssessmentResultType.php Peter
AssessmentStepType.php Peter
AwardedScope.php Peter
AwardedTo.php Peter
CandidateExpiryFilter.php Peter
CandidateRemovalReason.php Peter
CandidateSuspendedFilter.php Peter
CitizenshipStatus.php VD
EducationRequirementOption.php Matt
EducationStatus.php Matt
EducationType.php Matt
EstimatedLanguageAbility.php Matt
EvaluatedLanguageAbility.php Matt
GenericJobTitleKey.php Matt
GovEmployeeType.php Matt
IndigenousCommunity.php Matt
LanguageAbility.php Matt
Language.php Matt
OperationalRequirement.php Yoni
OverallAssessmentStatus.php Yoni
PlacementType.php Yoni
PoolCandidateSearchPositionType.php Yoni
PoolCandidateSearchRequestReason.php Yoni
PoolCandidateSearchStatus.php Yoni
PoolCandidateStatus.php Yoni
PoolLanguage.php Yoni
PoolOpportunityLength.php Yoni
PoolSkillType.php Yoni
PoolStatus.php VD
PoolStream.php VD
PriorityWeight.php VD
ProvinceOrTerritory.php VD
PublishingGroup.php VD
SecurityStatus.php VD
SkillCategory.php VD
SkillLevel.php VD
WorkRegion.php VD

@petertgiles
Copy link
Contributor

I need to go through this more carefully but so far I really like it.

Regarding the big array in GraphQLServiceProvider, I agree that it isn't great but isn't terrible either. It should be possible to fix this - Laravel itself does a lot of "if a file exists I'll use it" magic. At the same time, not terrible to have to do this manually for a bit.

Regarding the fallback for missing strings - I actually prefer the fallback to print the key. I think that's a lot more useful for a user than a generic "not found" string.

@codecov-commenter
Copy link

codecov-commenter commented Jun 27, 2024

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

Attention: Patch coverage is 46.92202% with 388 lines in your changes missing coverage. Please review.

Project coverage is 38.91%. Comparing base (a1c5532) to head (8befb77).
Report is 5 commits behind head on main.

Files Patch % Lines
...nts/ScreeningDecisions/ScreeningDecisionDialog.tsx 0.00% 26 Missing ⚠️
...ents/SearchRequestFilters/SearchRequestFilters.tsx 0.00% 21 Missing ⚠️
...web/src/components/PoolCandidatesTable/helpers.tsx 24.00% 19 Missing ⚠️
...omponents/PoolCandidatesTable/poolCandidateCsv.tsx 0.00% 15 Missing ⚠️
...eb/src/components/ScreeningDecisions/useOptions.ts 13.33% 13 Missing ⚠️
...BuilderPage/components/AssessmentDetailsDialog.tsx 0.00% 12 Missing ⚠️
...nts/Profile/components/LanguageProfile/Display.tsx 8.33% 11 Missing ⚠️
...Profile/components/PersonalInformation/Display.tsx 0.00% 11 Missing ⚠️
...ts/RemoveCandidateDialog/RemoveCandidateDialog.tsx 0.00% 10 Missing ⚠️
...ntPlanBuilderPage/components/SkillSummaryTable.tsx 0.00% 10 Missing ⚠️
... and 70 more
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #10701      +/-   ##
============================================
+ Coverage     38.60%   38.91%   +0.31%     
- Complexity     1654     1723      +69     
============================================
  Files          1013     1053      +40     
  Lines         30903    31236     +333     
  Branches       6439     6564     +125     
============================================
+ Hits          11929    12157     +228     
- Misses        18809    18912     +103     
- Partials        165      167       +2     
Flag Coverage Ξ”
integrationtests 68.49% <78.92%> (+0.72%) ⬆️
unittests 31.46% <31.08%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

β˜” View full report in Codecov by Sentry.
πŸ“’ Have feedback on the report? Share it here.

Copy link
Contributor

@mnigh mnigh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am finished with my list of enums.

I did notice this loading behaviour for some slow loading. Is this new with moving enums to the backend or is it something I hadn't noticed before when it was coming from the frontend?

Screen recording

slow_load.mov

api/lang/en/generic_job_title_key.php Outdated Show resolved Hide resolved
Copy link
Contributor

@vd1992 vd1992 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think I've run out of things to try and check, I leave it to the other reviewers now 🫑

@esizer
Copy link
Member Author

esizer commented Jul 4, 2024

I did notice this loading behaviour for some slow loading. Is this new with moving enums to the backend or is it something I hadn't noticed before when it was coming from the frontend?

Yeah, I was trying to avoid a bunch of prop drilling here and load the needed strings only when they were needed (opening the form). This can defintley be improved but I'm not sure how πŸ€”

  1. Query on page load and prop drill or,
  2. Show another spinner 😒

apps/web/src/pages/Users/UpdateUserPage/UpdateUserPage.tsx Outdated Show resolved Hide resolved
api/lang/en/placement_type.php Outdated Show resolved Hide resolved
Copy link
Contributor

@yonikid15 yonikid15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work Eric πŸ₯‡

Copy link
Contributor

@mnigh mnigh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a lot of πŸ‹οΈ going on here, nice work. the issues i've raised have been addressed. this is not part of the acceptance criteria so i guess i approve 🀷.

Copy link
Contributor

@petertgiles petertgiles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there! πŸ˜… A few small things and one big thing left.

packages/fake-data/src/fakeLocalizedEnum.ts Outdated Show resolved Hide resolved
packages/i18n/src/utils/enum.ts Outdated Show resolved Hide resolved
packages/i18n/src/lang/fr.json Outdated Show resolved Hide resolved
@esizer esizer requested a review from petertgiles July 5, 2024 17:45
@esizer esizer added this pull request to the merge queue Jul 5, 2024
Merged via the queue into main with commit 2a9ed10 Jul 5, 2024
9 checks passed
@esizer esizer deleted the 10634-move-localized-constants-to-backend branch July 5, 2024 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

♻️ Move localized constants to backend
8 participants