Skip to content

Commit

Permalink
docs: Make an editing pass on new translation docs
Browse files Browse the repository at this point in the history
Mostly this revises the text to be more fully oriented
toward giving instructions on what one needs to know
for working on the app, and in particular for adding
UI features.

Also add the fun and helpful fact that a hot reload is enough
to cause the bindings to get updated from the ARB files.

And tweak the descriptions on a couple of strings;
these label just a single item, not a section.
  • Loading branch information
gnprice committed Sep 15, 2023
1 parent 3165c81 commit 67dff51
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 47 deletions.
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,10 @@ The generated files that most frequently need an update are
run `flutter pub get && flutter build ios --config-only && flutter build macos --config-only`.


### Translation
### Translations and i18n

We currently have a framework for string translation in place that
incorporates the `flutter_localizations` package and has some
example usages.

For information on how the dart bindings are generated and how
to add new strings, refer to the [translation docs](docs/translation.md).
When adding new strings in the UI, we set them up to be translated.
For details on how to do this, see the [translation doc](docs/translation.md).


## License
Expand Down
4 changes: 2 additions & 2 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
},
"aboutPageOpenSourceLicenses": "Open-source licenses",
"@aboutPageOpenSourceLicenses": {
"description": "Section heading in About Zulip page to navigate to Open-source Licenses page"
"description": "Item title in About Zulip page to navigate to Licenses page"
},
"aboutPageTapToView": "Tap to view",
"@aboutPageTapToView": {
"description": "Button label in About Zulip page to navigate to Open-source Licenses page"
"description": "Item subtitle in About Zulip page to navigate to Licenses page"
},
"chooseAccountPageTitle": "Choose account",
"@chooseAccountPageTitle": {
Expand Down
130 changes: 92 additions & 38 deletions docs/translation.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,98 @@
# Translations

Our goal is for this app to be localized and offered in many
languages, just like zulip-mobile and zulip web.
languages, just like zulip-mobile and Zulip web.


## Current state

Currently in place is integration with `flutter_localizations`
package, allowing all flutter UI elements to be localized
We have a framework set up that makes it possible for UI strings
to be translated. (This was issue #275.) This means that when
adding new strings to the UI, instead of using a constant string
in English we'll add the string to that framework.
For details, see below.

At present not all of the codebase has been migrated to use the framework,
so you'll see some existing code that uses constant strings.
Fixing that is issue #277.

At present we don't have the strings wired up to a platform for
people to contribute translations. That's issue #276.
Until then, we have only a handful of strings actually translated,
just to make it possible to demonstrate the framework
is working correctly.


Per the discussion in #275 the approach here is to start with
ARB files and have dart autogenerate the bindings. I believe
this is the most straightforward way when connecting with a
translation management system, as they output ARB files that
we consume (this is also the same way web and mobile works
but with .po or .json files, I believe).
## Adding new UI strings

## Adding new strings
### Adding a string to the translation database

Add the appropriate entry in `assets/l10n/app_en.arb` ensuring
you add a corresponding resource attribute describing the
string in context. Example:
To add a new string in the UI, start by
adding an entry in the ARB file `assets/l10n/app_en.arb`.
This includes a name that you choose for the string,
its value in English,
and a "resource attribute" describing the string in context.
The name will become an identifier in our Dart code.
The description will provide context for people contributing translations.

For example, this entry describes a UI string
named `profileButtonSendDirectMessage`
which appears in English as "Send direct message":
```
"profileButtonSendDirectMessage": "Send direct message",
"@profileButtonSendDirectMessage": {
"description": "Label for button in profile screen to navigate to DMs with the shown user."
},
```

The bindings are automatically generated when you execute
`flutter run` although you can also manually trigger it
using `flutter gen-l10n`.
Then run the app (with `flutter run` or in your IDE),
or perform a hot reload,
to cause the Dart bindings to be updated based on your
changes to the ARB file.
(You can also trigger an update directly, with `flutter gen-l10n`.)

Untranslated strings will be included in a generated
`build/untranslated_messages.json` file. This output
awaits #276.

## Using in code
### Using a translated string in the code

To utilize in our widgets you need to import the generated
bindings:
To use in our widgets, you need to import the generated bindings:
```
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
```

And in your widget code pull the localizations out of the context:
Then in your widget code, pull the localizations object
off of the Flutter build context:
```
Widget build(BuildContext context) {
final zulipLocalizations = ZulipLocalizations.of(context);
```

And finally access one of the generated properties:
`Text(zulipLocalizations.chooseAccountButtonAddAnAccount)`.
Finally, on the localizations object use the getter
that was generated for the new string:
`Text(zulipLocalizations.profileButtonSendDirectMessage)`.


### Strings with placeholders

When a UI string is a constant per language, with no placeholders,
the generated Dart code provides a simple getter, as seen above.

When the string takes a placeholder,
the generated Dart binding for it will instead be a function,
taking arguments corresponding to the placeholders.

String that take placeholders are generated as functions
that take arguments: `zulipLocalizations.subscribedToNStreams(store.subscriptions.length)`
For example:
`zulipLocalizations.subscribedToNStreams(store.subscriptions.length)`.

## Hack to enforce locale (for testing, etc)

To manually trigger a locale change for testing I've found
it helpful to add the `localeResolutionCallback` in
`app.dart` to enforce a particular locale:
## Hack to enforce locale (for testing, etc.)

For testing the app's behavior in different locales,
you can use your device's system settings to
change the preferred language.

Alternatively, you may find it helpful to
pass a `localeResolutionCallback` to the `MaterialApp` in `app.dart`
to enforce a particular locale:

```
return GlobalStoreWidget(
Expand All @@ -75,16 +107,20 @@ return GlobalStoreWidget(
home: const ChooseAccountPage()));
```

(careful that returning a locale not in `supportedLocales`
will crash, the default behavior ensures a fallback is
always selected)
(When using this hack, returning a locale not in `supportedLocales` will
cause a crash.
The default behavior without `localeResolutionCallback` ensures
a fallback is always selected.)


## Tests

Widgets that access localization will fail if the root
`MaterialApp` given in the setup isn't also set up with
localizations. Make sure to add the right
`localizationDelegates` and `supportedLocales`:
Widgets that access localizations will fail if
the ambient `MaterialApp` isn't set up for localizations.
For the `MaterialApp` used in the app, we do this in `app.dart`.
In tests, this typically requires a test's setup code to provide
arguments `localizationDelegates` and `supportedLocales`.
For example:

```
await tester.pumpWidget(
Expand All @@ -95,3 +131,21 @@ localizations. Make sure to add the right
supportedLocales: ZulipLocalizations.supportedLocales,
home: PerAccountStoreWidget(
```


## Other notes

Our approach uses the `flutter_localizations` package.
We use the `gen_l10n` way, where we write ARB files
and the tool generates the Dart bindings.

As discussed in issue #275, the other way around was
also an option. But this way seems most straightforward
when connecting with a translation management system,
as they output ARB files that we consume.
This also parallels how zulip-mobile works with `.json` files
(and Zulip web, and the Zulip server with `.po` files?)

A file `build/untranslated_messages.json` is emitted
whenever the Dart bindings are generated from the ARB files.
This output awaits #276.

0 comments on commit 67dff51

Please sign in to comment.