-
Notifications
You must be signed in to change notification settings - Fork 306
Set up basic i18n framework #302
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c88b990
intl: Set up basic i18n framework
sirpengi 9db4310
intl: Change some hard-coded strings to be localized
sirpengi 3165c81
intl: Convert strings in `about_zulip.dart` to pull from translations
sirpengi 67dff51
docs: Make an editing pass on new translation docs
gnprice File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
{ | ||
"aboutPageTitle": "About Zulip", | ||
"@aboutPageTitle": { | ||
"description": "Title for About Zulip page" | ||
}, | ||
"aboutPageAppVersion": "App version", | ||
"@aboutPageAppVersion": { | ||
"description": "Label for Zulip app version in About Zulip page" | ||
}, | ||
"aboutPageOpenSourceLicenses": "Open-source licenses", | ||
"@aboutPageOpenSourceLicenses": { | ||
"description": "Item title in About Zulip page to navigate to Licenses page" | ||
}, | ||
"aboutPageTapToView": "Tap to view", | ||
"@aboutPageTapToView": { | ||
"description": "Item subtitle in About Zulip page to navigate to Licenses page" | ||
}, | ||
"chooseAccountPageTitle": "Choose account", | ||
"@chooseAccountPageTitle": { | ||
"description": "Title for ChooseAccountPage" | ||
}, | ||
"chooseAccountButtonAddAnAccount": "Add an account", | ||
"@chooseAccountButtonAddAnAccount": { | ||
"description": "Label for ChooseAccountPage button to add an account" | ||
}, | ||
"profileButtonSendDirectMessage": "Send direct message", | ||
"@profileButtonSendDirectMessage": { | ||
"description": "Label for button in profile screen to navigate to DMs with the shown user." | ||
}, | ||
"cameraAccessDeniedTitle": "Permissions needed", | ||
"@cameraAccessDeniedTitle": { | ||
"description": "Title for dialog when the user needs to grant permissions for camera access." | ||
}, | ||
"cameraAccessDeniedMessage": "To upload an image, please grant Zulip additional permissions in Settings.", | ||
"@cameraAccessDeniedMessage": { | ||
"description": "Message for dialog when the user needs to grant permissions for camera access." | ||
}, | ||
"cameraAccessDeniedButtonText": "Open settings", | ||
"@cameraAccessDeniedButtonText": { | ||
"description": "Message for dialog when the user needs to grant permissions for camera access." | ||
}, | ||
"subscribedToNStreams": "Subscribed to {num, plural, =0{no streams} =1{1 stream} other{{num} streams}}", | ||
"@subscribedToNStreams": { | ||
"description": "Test page label showing number of streams user is subscribed to.", | ||
"placeholders": { | ||
"num": { | ||
"type": "int", | ||
"example": "4" | ||
} | ||
} | ||
}, | ||
"userRoleOwner": "Owner", | ||
"@userRoleOwner": { | ||
"description": "Label for UserRole.owner" | ||
}, | ||
"userRoleAdministrator": "Administrator", | ||
"@userRoleAdministrator": { | ||
"description": "Label for UserRole.administrator" | ||
}, | ||
"userRoleModerator": "Moderator", | ||
"@userRoleModerator": { | ||
"description": "Label for UserRole.moderator" | ||
}, | ||
"userRoleMember": "Member", | ||
"@userRoleMember": { | ||
"description": "Label for UserRole.member" | ||
}, | ||
"userRoleGuest": "Guest", | ||
"@userRoleGuest": { | ||
"description": "Label for UserRole.guest" | ||
}, | ||
"userRoleUnknown": "Unknown", | ||
"@userRoleUnknown": { | ||
"description": "Label for UserRole.unknown" | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"chooseAccountPageTitle": "アカウントを選択", | ||
"chooseAccountButtonAddAnAccount": "新しいアカウントを追加", | ||
"profileButtonSendDirectMessage": "ダイレクトメッセージを送信", | ||
"cameraAccessDeniedTitle": "権限が必要です", | ||
"cameraAccessDeniedMessage": "画像をアップロードするには、「設定」で Zulip に追加の権限を許可してください。", | ||
"cameraAccessDeniedButtonText": "設定を開く", | ||
"subscribedToNStreams": "{num, plural, other{{num}つのストリームをフォローしています}}", | ||
"userRoleOwner": "オーナー", | ||
"userRoleAdministrator": "管理者", | ||
"userRoleModerator": "モデレータ", | ||
"userRoleMember": "メンバー", | ||
"userRoleGuest": "ゲスト", | ||
"userRoleUnknown": "不明" | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
# Translations | ||
|
||
Our goal is for this app to be localized and offered in many | ||
languages, just like zulip-mobile and Zulip web. | ||
|
||
|
||
## Current state | ||
|
||
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. | ||
|
||
|
||
## Adding new UI strings | ||
|
||
### Adding a string to the translation database | ||
|
||
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." | ||
}, | ||
``` | ||
|
||
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`.) | ||
|
||
|
||
### Using a translated string in the code | ||
|
||
To use in our widgets, you need to import the generated bindings: | ||
``` | ||
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; | ||
``` | ||
|
||
Then in your widget code, pull the localizations object | ||
off of the Flutter build context: | ||
``` | ||
Widget build(BuildContext context) { | ||
final zulipLocalizations = ZulipLocalizations.of(context); | ||
``` | ||
|
||
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. | ||
|
||
For example: | ||
`zulipLocalizations.subscribedToNStreams(store.subscriptions.length)`. | ||
|
||
|
||
## 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( | ||
child: MaterialApp( | ||
title: 'Zulip', | ||
localizationsDelegates: ZulipLocalizations.localizationsDelegates, | ||
supportedLocales: ZulipLocalizations.supportedLocales, | ||
localeResolutionCallback: (locale, supportedLocales) { | ||
return const Locale("ja"); | ||
}, | ||
theme: theme, | ||
home: const ChooseAccountPage())); | ||
``` | ||
|
||
(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 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( | ||
GlobalStoreWidget( | ||
child: MaterialApp( | ||
navigatorObservers: navigatorObserver != null ? [navigatorObserver] : [], | ||
localizationsDelegates: ZulipLocalizations.localizationsDelegates, | ||
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# Docs on this config file: | ||
# https://docs.flutter.dev/ui/accessibility-and-localization/internationalization#configuring-the-l10nyaml-file | ||
|
||
arb-dir: assets/l10n | ||
template-arb-file: app_en.arb | ||
required-resource-attributes: true | ||
output-localization-file: zulip_localizations.dart | ||
untranslated-messages-file: build/untranslated_messages.json | ||
output-class: ZulipLocalizations | ||
preferred-supported-locales: [ en ] | ||
nullable-getter: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From reading through those docs on this config file, one other option looks like it'd be good to add:
We can always relent on that if we decide it's a pain. But my guess is that it won't actually be much of a burden to write a few words about what the message is for, at the time when we're adding it and it's right in front of us. (And I see you've consistently done that in the demo messages in this PR.)