Skip to content

Commit 848d2fc

Browse files
committed
frontend/i18n: lang=... query parameter, refactor setLocale initialization
1 parent 9052551 commit 848d2fc

File tree

4 files changed

+55
-15
lines changed

4 files changed

+55
-15
lines changed

src/packages/frontend/app/page.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { Loading } from "@cocalc/frontend/components";
2727
import { IconName } from "@cocalc/frontend/components/icon";
2828
import { SiteName } from "@cocalc/frontend/customize";
2929
import { FileUsePage } from "@cocalc/frontend/file-use/page";
30-
import { getLocale, labels, Locale } from "@cocalc/frontend/i18n";
30+
import { labels } from "@cocalc/frontend/i18n";
3131
import { ProjectsNav } from "@cocalc/frontend/projects/projects-nav";
3232
import PayAsYouGoModal from "@cocalc/frontend/purchases/pay-as-you-go/modal";
3333
import openSupportTab from "@cocalc/frontend/support/open";
@@ -38,7 +38,6 @@ import { ConnectionIndicator } from "./connection-indicator";
3838
import { ConnectionInfo } from "./connection-info";
3939
import { useAppContext } from "./context";
4040
import { FullscreenButton } from "./fullscreen-button";
41-
import { useLocalizationCtx } from "./localize";
4241
import { AppLogo } from "./logo";
4342
import { NavTab } from "./nav-tab";
4443
import { Notification } from "./notifications";
@@ -76,7 +75,6 @@ export const Page: React.FC = () => {
7675
const { isNarrow, fileUseStyle, topBarStyle, projectsNavStyle } = pageStyle;
7776

7877
const intl = useIntl();
79-
const { setLocale } = useLocalizationCtx();
8078

8179
const open_projects = useTypedRedux("projects", "open_projects");
8280
const [show_label, set_show_label] = useState<boolean>(true);
@@ -105,7 +103,6 @@ export const Page: React.FC = () => {
105103
const account_id = useTypedRedux("account", "account_id");
106104
const is_logged_in = useTypedRedux("account", "is_logged_in");
107105
const is_anonymous = useTypedRedux("account", "is_anonymous");
108-
const other_settings = useTypedRedux("account", "other_settings");
109106
const doing_anonymous_setup = useTypedRedux(
110107
"account",
111108
"doing_anonymous_setup",
@@ -115,11 +112,6 @@ export const Page: React.FC = () => {
115112

116113
const is_commercial = useTypedRedux("customize", "is_commercial");
117114

118-
useEffect(() => {
119-
const i18n: Locale = getLocale(other_settings);
120-
setLocale(i18n);
121-
}, []);
122-
123115
function account_tab_icon(): IconName | JSX.Element {
124116
if (is_anonymous) {
125117
return <></>;

src/packages/frontend/app/render.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,51 @@
66
import ReactDOM from "react-dom";
77
import { createRoot } from "react-dom/client";
88

9-
import { Redux } from "@cocalc/frontend/app-framework";
9+
import {
10+
redux,
11+
Redux,
12+
useEffect,
13+
useTypedRedux,
14+
} from "@cocalc/frontend/app-framework";
15+
import {
16+
getLocale,
17+
Locale,
18+
LOCALIZATIONS,
19+
OTHER_SETTINGS_LOCALE_KEY,
20+
} from "@cocalc/frontend/i18n";
21+
import { QueryParams } from "@cocalc/frontend/misc/query-params";
1022
import { AppContext, useAppContextProvider } from "./context";
11-
import { Localize } from "./localize";
23+
import { Localize, useLocalizationCtx } from "./localize";
1224

1325
// App uses the context provided by Redux (for the locale, etc.) and Localize.
1426
function App({ children }) {
1527
const appState = useAppContextProvider();
28+
const { setLocale } = useLocalizationCtx();
29+
const other_settings = useTypedRedux("account", "other_settings");
30+
31+
useEffect(() => {
32+
const lang = QueryParams.get("lang");
33+
if (lang != null) {
34+
if (lang in LOCALIZATIONS) {
35+
console.warn(
36+
`URL query parameter 'lang=${lang}' – overriding user configuration.`,
37+
);
38+
redux
39+
.getActions("account")
40+
.set_other_settings(OTHER_SETTINGS_LOCALE_KEY, lang);
41+
setLocale(lang);
42+
} else {
43+
console.warn(
44+
`URL query parameter 'lang=${lang}' provided, but not a valid locale.`,
45+
`Known values: ${Object.keys(LOCALIZATIONS)}`,
46+
);
47+
}
48+
} else {
49+
const i18n: Locale = getLocale(other_settings);
50+
setLocale(i18n);
51+
}
52+
}, [other_settings]);
53+
1654
return <AppContext.Provider value={appState}>{children}</AppContext.Provider>;
1755
}
1856

src/packages/frontend/i18n/README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# I18N in CoCalc
22

3-
Define messages directly in the component or use `./common.ts` for shared messages, etc. Search for e.g. `labels.projects` to see how they are used. This is only for the frontend – the Next.js part could be done similar, but needs a separate workflow.
3+
## Development
4+
5+
Define messages directly in the component or use `./common.ts` for shared messages, etc. Search for e.g. `labels.projects` to see how they are used. This is only for the frontend – the Next.js part could be done similarly, but needs a separate workflow.
46

57
`frontend/package.json` has three script entries, which are for [SimpleLocalize](https://simplelocalize.io/).
68

@@ -42,10 +44,18 @@ CoCalc specific rules for implementing translations, of which I think are good t
4244

4345
- **Explicit ID**: Technically, the ID is optional. Then it is computed as a hash upon extraction. However, this has two negative sides:
4446
- If the message changes, it's hash changes, and you have to start over with the translation. This is good from an idealistic standpoint, but if you just tweak a word or correct a typo, the existing translations are still ok. If the meaning changes completely, it's better to create a new ID. (Of course, changes to the `defaultMessage` need to go through the `extract → upload` step, except that the English translation uses the `defaultMessage` directly.)
45-
- Sorting: All the translations and also online tools like SimpleLocalize sort the translations by their keys. Look at the translated `i18n/de_DE.json` and you'll see, that messages that are related are also next to each other. This also makes it possible to filter for a specific
47+
- Sorting: All the translations and also online tools like SimpleLocalize sort the translations by their keys. Look at the translated `i18n/de_DE.json` and you'll see that messages that are related are also next to each other. This also makes it possible to filter for a specific
4648
- Pitfall **No variables in properties**: I think the extraction process does not know how to deal with variables, when extracting strings from properties. So, either define the message objects elsewhere (like it is done with `labels`) or write a multiline string in place. See the examples below for what works.
4749
- **No `en` translation**: English is the default. The `defaultMessage` is already in the code. We do not download the supposedly translated `en` file and just let the fallback mechanism kick in. This also means that changes to the `defaultMessage` will show up with the next build, even without touching any of the translations.
4850
- **richTextElements**: in `app/localize.tsx`, a few default `richTextElements` are defined – just for convenience. Anchor tags must be defined individually, because link text and href can't be wrapped that way.
51+
- **Query parameter**: A new `?lang=en` (or `=de`, `=zh`, ...) query parameters lets you change the language as well. This also changes the account setting. Hence, a URL with `?lang=en` can be used to reset the account setting to English.
52+
53+
## Style
54+
55+
We discussed this internally on 2024-08-19 and came to the conclusion that we should not overdo translations.
56+
57+
- Example: translating the "Run" button in Jupyter to German or Russian, which both have more or less the meaning of "running in the street", is extremely awkward. It's also a well recognizable element, which users are used to, even without knowing what it really means. Therefore, we do not translate elements like the "Run" button.
58+
- However, what we should translate (or add) are hover text explanations. So, in the case of the "Run" button, there should be a tooltip, which explains in the current language, what this button really does.
4959

5060
## Examples
5161

@@ -119,7 +129,7 @@ force_build: {
119129
}
120130
```
121131

122-
The `ManageCommands` class knows what to do with that. If you use a function to deinfe a label or title,
132+
The `ManageCommands` class knows what to do with that. If you use a function to define a label or title,
123133
the `intl` (`IntlShape`) object is passed in as well.
124134

125135
```typescript

src/packages/frontend/misc/misc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6-
import { QueryParams } from "../misc/query-params";
76
import target from "@cocalc/frontend/client/handle-target";
7+
import { QueryParams } from "@cocalc/frontend/misc/query-params";
88

99
declare var $: any;
1010

0 commit comments

Comments
 (0)