diff --git a/src/core/client/admin/components/UserHistoryDrawer/RecentHistory.tsx b/src/core/client/admin/components/UserHistoryDrawer/RecentHistory.tsx index 93945922d..0ff568db7 100644 --- a/src/core/client/admin/components/UserHistoryDrawer/RecentHistory.tsx +++ b/src/core/client/admin/components/UserHistoryDrawer/RecentHistory.tsx @@ -2,7 +2,7 @@ import cn from "classnames"; import { Localized } from "fluent-react/compat"; import React, { FunctionComponent } from "react"; -import { reduceSeconds, UNIT } from "coral-framework/lib/i18n"; +import { reduceSeconds, UNIT } from "coral-common/helpers/i18n"; import { Box, Flex, diff --git a/src/core/client/admin/components/UserHistoryDrawer/SuspensionAction.tsx b/src/core/client/admin/components/UserHistoryDrawer/SuspensionAction.tsx index 009d65eed..81a84122b 100644 --- a/src/core/client/admin/components/UserHistoryDrawer/SuspensionAction.tsx +++ b/src/core/client/admin/components/UserHistoryDrawer/SuspensionAction.tsx @@ -1,7 +1,7 @@ import { Localized } from "fluent-react/compat"; import React, { FunctionComponent } from "react"; -import { reduceSeconds } from "coral-framework/lib/i18n"; +import { reduceSeconds } from "coral-common/helpers/i18n"; interface From { start: Date; diff --git a/src/core/client/admin/components/UserStatus/SuspendForm.tsx b/src/core/client/admin/components/UserStatus/SuspendForm.tsx index 1eabcbfa7..71e9e2343 100644 --- a/src/core/client/admin/components/UserStatus/SuspendForm.tsx +++ b/src/core/client/admin/components/UserStatus/SuspendForm.tsx @@ -3,12 +3,8 @@ import { Localized } from "fluent-react/compat"; import React, { FunctionComponent, useCallback } from "react"; import { Field, Form } from "react-final-form"; -import { - GetMessage, - ScaledUnit, - withGetMessage, -} from "coral-framework/lib/i18n"; - +import { ScaledUnit } from "coral-common/helpers/i18n"; +import { GetMessage, withGetMessage } from "coral-framework/lib/i18n"; import { Button, CheckBox, diff --git a/src/core/client/admin/components/UserStatus/SuspendModal.tsx b/src/core/client/admin/components/UserStatus/SuspendModal.tsx index a50f10d0d..c8cd7bdd0 100644 --- a/src/core/client/admin/components/UserStatus/SuspendModal.tsx +++ b/src/core/client/admin/components/UserStatus/SuspendModal.tsx @@ -1,13 +1,9 @@ import { Localized } from "fluent-react/compat"; import React, { FunctionComponent, useCallback, useState } from "react"; -import { - GetMessage, - ScaledUnit, - withGetMessage, -} from "coral-framework/lib/i18n"; - import NotAvailable from "coral-admin/components/NotAvailable"; +import { ScaledUnit } from "coral-common/helpers/i18n"; +import { GetMessage, withGetMessage } from "coral-framework/lib/i18n"; import { Button, Card, diff --git a/src/core/client/framework/components/DurationField.tsx b/src/core/client/framework/components/DurationField.tsx index d63e5fedf..e5fd4769c 100644 --- a/src/core/client/framework/components/DurationField.tsx +++ b/src/core/client/framework/components/DurationField.tsx @@ -1,7 +1,7 @@ import { Localized } from "fluent-react/compat"; import React, { ChangeEvent, Component } from "react"; -import { UNIT } from "coral-framework/lib/i18n"; +import { UNIT } from "coral-common/helpers/i18n"; import { Flex, Option, diff --git a/src/core/client/framework/lib/i18n/index.ts b/src/core/client/framework/lib/i18n/index.ts index a5dab4f05..c5d064651 100644 --- a/src/core/client/framework/lib/i18n/index.ts +++ b/src/core/client/framework/lib/i18n/index.ts @@ -3,8 +3,3 @@ export { default as negotiateLanguages } from "./negotiateLanguages"; export { BundledLocales, LoadableLocales, LocalesData } from "./locales"; export { default as getMessage } from "./getMessage"; export { default as withGetMessage, GetMessage } from "./withGetMessage"; -export { - default as reduceSeconds, - UNIT, - ScaledUnit, -} from "../../../../common/helpers/i18n/reduceSeconds"; diff --git a/src/core/client/stream/tabs/Profile/ChangeUsername/ChangeUsernameContainer.tsx b/src/core/client/stream/tabs/Profile/ChangeUsername/ChangeUsernameContainer.tsx index e3d4667f4..cb6ee011c 100644 --- a/src/core/client/stream/tabs/Profile/ChangeUsername/ChangeUsernameContainer.tsx +++ b/src/core/client/stream/tabs/Profile/ChangeUsername/ChangeUsernameContainer.tsx @@ -10,7 +10,7 @@ import React, { import { Field, Form } from "react-final-form"; import { ALLOWED_USERNAME_CHANGE_FREQUENCY } from "coral-common/constants"; -import reduceSeconds, { UNIT } from "coral-common/helpers/i18n/reduceSeconds"; +import { reduceSeconds, UNIT } from "coral-common/helpers/i18n"; import { InvalidRequestError } from "coral-framework/lib/errors"; import { ValidationMessage } from "coral-framework/lib/form"; import { @@ -146,11 +146,11 @@ const ChangeUsernameContainer: FunctionComponent = ({ viewer }) => { )} {!showEditForm && ( - + {viewer.username} @@ -166,13 +166,17 @@ const ChangeUsernameContainer: FunctionComponent = ({ viewer }) => { } $value={FREQUENCYSCALED.scaled} $unit={FREQUENCYSCALED.unit} > Change the username that will appear on all of your past and - future comments. Usernames can be changed once every{" "} - {FREQUENCYSCALED.scaled} {FREQUENCYSCALED.unit} + future comments.{" "} + + Usernames can be changed once every {FREQUENCYSCALED.scaled}{" "} + {FREQUENCYSCALED.unit}. + @@ -260,10 +264,15 @@ const ChangeUsernameContainer: FunctionComponent = ({ viewer }) => { Cancel - + save} + span={} + > diff --git a/src/core/client/stream/test/profile/changeUsername.spec.tsx b/src/core/client/stream/test/profile/changeUsername.spec.tsx index ad9342daf..da9857e1b 100644 --- a/src/core/client/stream/test/profile/changeUsername.spec.tsx +++ b/src/core/client/stream/test/profile/changeUsername.spec.tsx @@ -69,7 +69,7 @@ describe("with recently changed username", () => { within(testRenderer.root).queryByTestID("profile-changeUsername") ); within(changeUsername).getByText("u_changed"); - const editButton = within(changeUsername).getByText("Edit"); + const editButton = within(changeUsername).getByText("edit"); act(() => { editButton.props.onClick(); }); @@ -102,7 +102,7 @@ describe("with new username", () => { ); within(changeUsername).getByText("u_original"); - const editButton = within(changeUsername).getByText("Edit"); + const editButton = within(changeUsername).getByText("edit"); act(() => { editButton.props.onClick(); }); @@ -145,7 +145,7 @@ describe("change username form", () => { const changeUsername = within(testRenderer.root).getByTestID( "profile-changeUsername" ); - const editButton = within(changeUsername).getByText("Edit"); + const editButton = within(changeUsername).getByText("edit"); act(() => { editButton.props.onClick(); }); @@ -156,7 +156,9 @@ describe("change username form", () => { within(changeUsername).getAllByText("This field is required", { exact: false, }); - const button = within(changeUsername).getByText("Save"); + const button = within(changeUsername).getByTestID( + "profile-changeUsername-save" + ); expect(button.props.disabled).toBeTruthy(); }); @@ -164,7 +166,7 @@ describe("change username form", () => { const changeUsername = within(testRenderer.root).getByTestID( "profile-changeUsername" ); - const editButton = within(changeUsername).getByText("Edit"); + const editButton = within(changeUsername).getByText("edit"); act(() => { editButton.props.onClick(); }); @@ -181,7 +183,9 @@ describe("change username form", () => { within(changeUsername).getByText("Usernames do not match. Try again.", { exact: false, }); - const button = within(changeUsername).getByText("Save"); + const button = within(changeUsername).getByTestID( + "profile-changeUsername-save" + ); expect(button.props.disabled).toBeTruthy(); }); @@ -189,7 +193,7 @@ describe("change username form", () => { const changeUsername = within(testRenderer.root).getByTestID( "profile-changeUsername" ); - const editButton = within(changeUsername).getByText("Edit"); + const editButton = within(changeUsername).getByText("edit"); act(() => { editButton.props.onClick(); }); diff --git a/src/core/common/helpers/i18n/index.ts b/src/core/common/helpers/i18n/index.ts new file mode 100644 index 000000000..7f0716035 --- /dev/null +++ b/src/core/common/helpers/i18n/index.ts @@ -0,0 +1,3 @@ +export * from "./locales"; +export { default as reduceSeconds } from "./reduceSeconds"; +export * from "./reduceSeconds"; diff --git a/src/core/server/errors/index.ts b/src/core/server/errors/index.ts index 8007eb2ad..b49cbd74f 100644 --- a/src/core/server/errors/index.ts +++ b/src/core/server/errors/index.ts @@ -6,7 +6,7 @@ import { VError } from "verror"; import { ALLOWED_USERNAME_CHANGE_FREQUENCY } from "coral-common/constants"; import { ERROR_CODES, ERROR_TYPES } from "coral-common/errors"; -import reduceSeconds, { UNIT } from "coral-common/helpers/i18n/reduceSeconds"; +import { reduceSeconds, UNIT } from "coral-common/helpers/i18n"; import { translate } from "coral-server/services/i18n"; import { Writeable } from "coral-common/types"; diff --git a/src/core/server/graph/tenant/mutators/Users.ts b/src/core/server/graph/tenant/mutators/Users.ts index 4cff2f07f..1c9a5d56a 100644 --- a/src/core/server/graph/tenant/mutators/Users.ts +++ b/src/core/server/graph/tenant/mutators/Users.ts @@ -128,7 +128,8 @@ export const Users = (ctx: TenantContext) => ({ ctx.mailerQueue, ctx.tenant, ctx.user!, - input.username + input.username, + ctx.now ), updateUserUsername: async (input: GQLUpdateUserUsernameInput) => updateUsernameByID( diff --git a/src/core/server/services/i18n/index.ts b/src/core/server/services/i18n/index.ts index 47c762ce9..a2248f2ef 100644 --- a/src/core/server/services/i18n/index.ts +++ b/src/core/server/services/i18n/index.ts @@ -4,6 +4,7 @@ import path from "path"; import { LanguageCode, LOCALES } from "coral-common/helpers/i18n/locales"; import config from "coral-server/config"; +import logger from "coral-server/logger"; /** * isLanguageCode will return true if the string is a `LanguageCode`. @@ -14,8 +15,13 @@ function isLanguageCode(locale: string): locale is LanguageCode { return LOCALES.some(code => code === locale); } -// pathToLocales is the path where the server stores the locales. -const pathToLocales = path.join(__dirname, "..", "..", "locales"); +// pathsToLocales is the paths where to find the locales. +const pathsToLocales = [ + // Client locales. + path.join(__dirname, "..", "..", "..", "..", "locales"), + // Server locales. + path.join(__dirname, "..", "..", "locales"), +]; export class I18n { private bundles: Partial> = {}; @@ -29,35 +35,36 @@ export class I18n { * load will read all the translations located in the server locales folder. */ public async load() { - // Load all the locales from the server locales folder. - // Load all the locales from the locales folders. - const folders = await fs.readdir(pathToLocales); + for (const localesFolder of pathsToLocales) { + const folders = await fs.readdir(localesFolder); - // Load all the translation files for each of the folders. - for (const folder of folders) { - // Parse out the language code. - const locale = path.basename(folder); - if (!isLanguageCode(locale)) { - throw new Error(`invalid language code: ${locale}`); + // Load all the translation files for each of the folders. + for (const folder of folders) { + // Parse out the language code. + const locale = path.basename(folder); + if (!isLanguageCode(locale)) { + throw new Error(`invalid language code: ${locale}`); + } + + // Now we have a language code. + const bundle: FluentBundle = + this.bundles[locale] || new FluentBundle(locale); + + // Load all the translations in the folder. + const files = await fs.readdir(path.join(localesFolder, folder)); + + for (const file of files) { + const filePath = path.join(localesFolder, folder, file); + logger.debug({ locale, filePath }, "loading messages for locale"); + + const messages = await fs.readFile(filePath, "utf8"); + + bundle.addMessages(messages); + } + + this.bundles[locale] = bundle; } - - // Now we have a language code. - const bundle = new FluentBundle(locale); - - // Load all the translations in the folder. - const files = await fs.readdir(path.join(pathToLocales, folder)); - - for (const file of files) { - const messages = await fs.readFile( - path.join(pathToLocales, folder, file), - "utf8" - ); - - bundle.addMessages(messages); - } - - this.bundles[locale] = bundle; } } diff --git a/src/core/server/services/users/index.ts b/src/core/server/services/users/index.ts index adcdc8c50..c628431b6 100644 --- a/src/core/server/services/users/index.ts +++ b/src/core/server/services/users/index.ts @@ -382,20 +382,24 @@ export async function updateUsername( mailer: MailerQueue, tenant: Tenant, user: User, - username: string + username: string, + now: Date ) { // Validate the username. validateUsername(username); - const lastUsernameEditAllowed = new Date(); - const dateDiff = - lastUsernameEditAllowed.getSeconds() - ALLOWED_USERNAME_CHANGE_FREQUENCY; - lastUsernameEditAllowed.setDate(dateDiff); + // Get the earliest date that the username could have been edited before to/ + // allow it now. + const lastUsernameEditAllowed = DateTime.fromJSDate(now) + .plus({ seconds: -ALLOWED_USERNAME_CHANGE_FREQUENCY }) + .toJSDate(); const { history } = user.status.username; if (history.length > 1) { + // If the last update was made at a date sooner than the earliest edited + // date, then we know that the last edit was conducted within the time-frame + // already. const lastUpdate = history[history.length - 1]; - if (lastUpdate.createdAt > lastUsernameEditAllowed) { throw new UsernameUpdatedWithinWindowError(lastUpdate.createdAt); } diff --git a/src/locales/en-US/stream.ftl b/src/locales/en-US/stream.ftl index e704ba5e1..4f9f0ca83 100644 --- a/src/locales/en-US/stream.ftl +++ b/src/locales/en-US/stream.ftl @@ -221,14 +221,14 @@ configure-configureQuery-storyNotFound = Story not found ## Change username profile-changeUsername-success = Your username has been successfully updated -profile-changeUsername-edit = Edit +profile-changeUsername-edit = edit profile-changeUsername-heading = Edit your username -profile-changeUsername-desc = Change the username that will appear on all of your past and future comments. Usernames can be changed once every { framework-timeago-time }. +profile-changeUsername-desc = Change the username that will appear on all of your past and future comments. Usernames can be changed once every { framework-timeago-time }. profile-changeUsername-current = Current username profile-changeUsername-newUsername-label = New username profile-changeUsername-confirmNewUsername-label = Confirm new username profile-changeUsername-cancel = Cancel -profile-changeUsername-submit = Save +profile-changeUsername-submit = save Save profile-changeUsername-recentChange = Your username has been changed in the last { framework-timeago-time }. You may change your username again on { $nextUpdate } profile-changeUsername-close = Close