Conversation
|
|
@garethbowen There's still plenty to do, but the PR is at a good point to share for feedback. |
|
|
||
| const formPreviewState = ref<FormPreviewState>(); | ||
|
|
||
| // TODO: REMOVE THIS MOCK. Temporal for testing translations |
There was a problem hiding this comment.
I will revert the changes once the task is completed.
| return [namespace, module.defaultStrings]; | ||
| }); | ||
|
|
||
| const resolvedImports = await Promise.all(imports); |
There was a problem hiding this comment.
I don't think you need to do await Promise.all here because you're awaiting in the .map right?
| const files = await findI18nFiles(srcDir); | ||
| const dictionary = await buildAggregatedDictionary(files); | ||
|
|
||
| await mkdir(outputDir, { recursive: true }); |
There was a problem hiding this comment.
Will the outputdir be ignored or committed? If it's ignored, let's call it something more obvious, like build/locales or something?
| } | ||
| }; | ||
|
|
||
| void runExtraction(); |
There was a problem hiding this comment.
Central has done a lot of work with scripts to do this too - is there any benefit in trying to reuse their effort?
There was a problem hiding this comment.
Still different, this is a lot simpler
| import type { TranslationDictionary } from '@/lib/i18n/i18n-context.ts'; | ||
|
|
||
| export const defaultStrings: TranslationDictionary = { | ||
| title: { |
There was a problem hiding this comment.
Probably not a good label for an error message.
| if (violationLength === 0) return ''; | ||
| else if (violationLength === 1) return '1 question with error.'; | ||
| else return `${violationLength} questions with errors.`; | ||
| if (violationLength === 0) { |
| developer_comment: | ||
| 'Message shown in the error banner when multiple questions have validation errors. {count} is the number of errors.', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
It'd be better to combine these into one message with logic to handle the plurals.
| ) => Promise<Record<string, TranslationDictionary>>; | ||
|
|
||
| export function useI18nSetup(fetchTranslations?: FetchTranslationsCallback) { | ||
| const browserLocale = navigator.language?.split('-')[0]; |
There was a problem hiding this comment.
It feels like we're having to implement things that a property translation lib should do for us. This is one example, where maybe it's good enough, but we're missing out on features you get for free using a library directly.
There was a problem hiding this comment.
The vue-i18n doesn't determine the language, instead, Central retrieves it from navigator.language as we do here, and also from localStorage when their app language dropdown is changed.
There was a problem hiding this comment.
Right but it does do proper locale fallback so you (a) don't need to split the language from the navigator, and (b) get the flexibility of being able to use the second part for more flexibility, (eg: en-NZ: "initialising" vs en-US: "initializing").
| }; | ||
|
|
||
| return { t }; | ||
| } |
There was a problem hiding this comment.
I'm nervous about providing custom code and caching, and would lean towards a more complete library such as vue-i18n which does more for us. Plus we have the benefit of standardisation with central and benefiting from things they've learned and tools they've built.
There was a problem hiding this comment.
Yes, I understand the concern about the code and caching (I can improve the implementation to make it more robust).
I have two takes on the vue-i18n vs. intl-messageformat choice:
1. WF as a separate project using standard ICU MessageFormat
(This was my initial assumption for the design explained in the PR description)
The deeper I look into vue-i18n integration with Transifex, the less it makes sense for this project. Central uses a massive custom build script (~750 lines) to make it work with Transifex and ICU plurals (+ other utils around the code). They had to write a custom parser to rip apart ICU strings and convert them into vue-i18n’s pipe syntax. Even though that code is mature, I’m uncomfortable importing that complexity into the Web Forms repo. Plus, since we fetch translations dynamically, using vue-i18n with ICU would mean shipping that compiler code to the browser (I need to test this).
Our bundle size is already big. The vue-i18n plus the custom scripts adds >20kb, while intl-messageformat is only 9kb. We could save that space for other features :)
If this works well, Central might even be able to adopt this approach later to align better with native APIs and remove a lot of the custom code they need to maintain.
2. WF has a strong dependency on Central and WF uses the vue-i18n format
In this case, Central would handle the conversion of ICU MessageFormat and send the vue-i18n format directly to Web Forms. But we’d still have a larger bundle size than intl-messageformat, and we'd be "locked in" to Central's decisions on format.
I’m going to set up a meeting with the Central team to understand their struggles with Transifex and how it impacts their vue-i18n usage. Let me know if you still feel strongly about aligning with Central on this specific library and we can plan how to share code, etc.
|
The open question is really interesting. We could add to the spec to make this deterministic, eg: |
|
Great to see this moving forward! I appreciate the thought given to keeping the default bundle small and leaving open the possibility of Central users supplying their own translations. I haven't given any thought to
https://docs.getodk.org/form-language/ -- we do document adding the IANA language code in parentheses and XLSForm warns when it's not included. enketo/enketo#1082 has the algorithm we had come up with for determining form and UI language in Enketo. I think eventually we would like the same in Web Forms but it can be a follow-up issue to refine (e.g. if form-level default language is currently ignored). |
Closes #332
TODO
I have verified this PR works in these browsers (latest versions):
What else has been done to verify that this works as intended?
Browsers with different default languages
See message in error banner translated
Why is this the best possible solution? Were any other approaches considered?
The objective is to implement a scalable, lightweight internationalization that:
Boundary definition:
Web Forms will manage its own internal locale state and browser detection, but it will remain agnostic to how translation files are retrieved (Transifex).
navigator.language) upon initializationfetchTranslationsfunction passed to Web Forms, handling the actual network request to fetch the appropriate JSON translation file from the backend or Transifex.Translation format:
Web Forms will utilize a nested JSON structure with strings and developer comments to align with Central and Collect, following ICU Message Format syntax for handling variables and plurals.
It will include TypeScript interfaces to ensure a valid structure and documentation to guide contributors.
Library:
To parse the ICU strings, Web Forms will use
intl-messageformat(from FormatJS) rather than heavier frameworks.Other libraries considered
Implementation details:
Each component that needs translations will have a sibling file:
<component>.i18n.tsThis separation maintains component readability and focus on code. It is also extensible; if additional languages need to be added in the future, the main component remains unaffected. And good for tree-shaking support (more on that below).
There's a script (package/web-forms/script/extract-translations.js) that dynamically gets all
<component>.i18n.tsand compiles them into a singlestrings_en.jsonfile for Transifex upload. Maybe the upload can be automated as part of the release step in Github Actions.It uses one Vue composable to initialize the internalization and fetch the translation files from the Host app. Another Vue composable keeps the locale state and executes the
intl-messageformatparser.Components will access strings via dot-notation:
<span>{{ t('errors.invalidInputs', { count: 3 }) }}</span>Possible future considerations (not part of this delivery)
destructure-translations.jsscript that takes the downloaded translated JSON from Transifex and maps the localized strings back to their respective component i18n, allowing the application to easily serve them on request (tree-shaking for all languages, not just the default language).*.i18n.[locale].tsdirectly. The current structure ensures zero refactoring in the Vue templates/components.How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?
Do we need any specific form for testing your changes? If so, please attach one.
What's changed
Open Question:
If switching language from the dropdown

How to get the locale code if form definitions don't provide the standard: