Skip to content
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

Feats: Add localization #2321

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0dcb75d
chore: add instructions to contributing guide
thomasync Sep 26, 2023
00845d8
feats: add localization
thomasync Sep 28, 2023
bbb4baf
feats(localization): added a script to automate locales search
thomasync Sep 28, 2023
783b139
feats(localization): add a script to help in development mode or relo…
thomasync Sep 28, 2023
46720ec
fix(localization): improved error detection
thomasync Sep 28, 2023
94653bc
fix(localization): improve spaces
thomasync Sep 29, 2023
c337b58
feats(localization): add FR locale
thomasync Sep 29, 2023
93d5b4c
feats(localization): replace strings in all files
thomasync Sep 29, 2023
3e57a1a
Merge remote-tracking branch 'upstream/develop' into feats-localization
thomasync Sep 29, 2023
40cbad2
chore: fix discord link
thomasync Sep 29, 2023
7c50bed
feats(localization): add string to files
thomasync Oct 3, 2023
2639263
fix: round number because can be infinite .333333
thomasync Oct 3, 2023
5a152c8
fix(localizations): hydratation error. moved localStorage to cookies …
thomasync Oct 3, 2023
0af0812
feats(localization): replace strings in all files
thomasync Oct 3, 2023
c244fbd
feats(localization): format date from greetings
thomasync Oct 3, 2023
fe6faf8
chore(localization): minor fixes
thomasync Oct 3, 2023
6a78f1a
Merge remote-tracking branch 'upstream/develop' into feats-localization
thomasync Oct 3, 2023
9054c74
Merge remote-tracking branch 'upstream/develop' into feats-localization
thomasync Oct 4, 2023
37e2a75
chore(localization): update FR locale
thomasync Oct 4, 2023
07d9272
chore(localization): add strings
thomasync Oct 4, 2023
6cf49c8
Merge remote-tracking branch 'upstream/develop' into feats-localization
thomasync Oct 4, 2023
118464c
Merge remote-tracking branch 'upstream/develop' into feats-localization
thomasync Oct 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,10 @@ package-lock.json
package-lock.json
pnpm-lock.yaml
pnpm-workspace.yaml
bun.lockb

.npmrc

# locales
web/public/locales
space/public/locales
1,073 changes: 1,073 additions & 0 deletions locales/en_US.json

Large diffs are not rendered by default.

1,073 changes: 1,073 additions & 0 deletions locales/fr_FR.json

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions scripts/localization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const fs = require('fs');
const path = require('path');

const PATH_TO_LANG = path.resolve(__dirname, '../locales');
const PATH_TO_ENGLISH = path.join(PATH_TO_LANG, 'en_US.json');

const sourceTerms = [];
let found = 0;

const files = [];
collectFiles(__dirname + '/../web');


files.forEach((file) => {
const file_content = fs.readFileSync(file).toString();
const match = /localized?\([\n\t\s]*['`"](.*?)['`"][\n\t\s]*(?:\,|\)(?!['`"]))/gs;
const localized_matches = file_content.matchAll(match);

if (localized_matches) {
for (const localized_match of localized_matches) {
let localized_string = localized_match[1];
// Replace concatenation of strings to a single string
localized_string = localized_string.replace(/['`"][\s\n]*\+[\s\n]*['`"]/g, '').replace(/\s+/g, ' ');

// add to sourceTerms
sourceTerms[localized_string] = localized_string;

found += 1;
}
}
});

console.log('\nUpdating en_US.json to match strings in source:');
console.log(`- ${found} localized() calls, ${Object.keys(sourceTerms).length} strings`);

writeTerms(sourceTerms, PATH_TO_ENGLISH);

removeUnused();
updateLocales();

function collectFiles(dir) {
fs.readdirSync(dir).forEach(file => {
const p = path.join(dir, file);
if (fs.lstatSync(p).isDirectory()) {
collectFiles(p);
} else if (p.endsWith('.ts') || p.endsWith('.tsx')) {
files.push(p);
}
});
}

function writeTerms(terms, destPath) {
const ordered = {};
Object.keys(terms)
.sort()
.forEach(function (key) {
ordered[key] = terms[key];
});
fs.writeFileSync(destPath, JSON.stringify(ordered, null, 4));
}

function removeUnused() {
console.log('\nRemoving unused localized strings:');
console.log('Lang\t\tStrings\t\t\tPercent');
console.log('------------------------------------------------');
fs.readdirSync(PATH_TO_LANG).forEach(filename => {
if (!filename.endsWith('.json')) return;
const localePath = path.join(PATH_TO_LANG, filename);
const localized = JSON.parse(fs.readFileSync(localePath).toString());

const inuse = Object.keys(localized)
.filter(term => sourceTerms[term])
.reduce((obj, term) => {
obj[term] = localized[term];
return obj;
}, {});

if (inuse) {
writeTerms(inuse, localePath);
}

const inuse_length = Object.keys(inuse).length;
const source_length = Object.keys(sourceTerms).length;
const lang = path.basename(filename, '.json');

console.log(
`- ${lang}\t${lang.length < 6 ? '\t' : ''}${inuse_length}\t/ ${source_length}\t\t${Math.round(inuse_length / source_length * 100)}%`
);
});
}

function updateLocales() {
const english = JSON.parse(fs.readFileSync(PATH_TO_ENGLISH).toString());

console.log('\nAdding missing localized strings:');
console.log('Lang\t\tStrings\t\t\tPercent');
console.log('------------------------------------------------');
fs.readdirSync(PATH_TO_LANG).forEach(filename => {
if (!filename.endsWith('.json') || filename === "en_US.json") return;
const localePath = path.join(PATH_TO_LANG, filename);
const localized = JSON.parse(fs.readFileSync(localePath).toString());
let not_localized = 0;

Object.keys(english).forEach(term => {
if (!localized[term]) {
localized[term] = null;
not_localized += 1;
}
});

if (localized) {
writeTerms(localized, localePath);
}

const english_length = Object.keys(english).length;
const lang = path.basename(filename, '.json');

console.log(
`- ${lang}\t${lang.length < 6 ? '\t' : ''}${not_localized}\t/ ${english_length}\t\t${Math.round(not_localized / english_length * 100)}%`
);
});
console.log('\nDone!');
}
6 changes: 6 additions & 0 deletions scripts/reload-locales.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

DIR="$(realpath `dirname $0`/..)"

cp -r $DIR/locales $DIR/web/public/
cp -r $DIR/locales $DIR/space/public/
4 changes: 4 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ cp ./web/.env.example ./web/.env
cp ./space/.env.example ./space/.env
cp ./apiserver/.env.example ./apiserver/.env

# copy locales
cp -r ./locales ./web/public/
cp -r ./locales ./space/public/

# Generate the SECRET_KEY that will be used by django
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import useToast from "hooks/use-toast";
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// types
import { IAnalyticsParams, ISaveAnalyticsFormData } from "types";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";

// types
type Props = {
Expand All @@ -33,6 +36,7 @@ const defaultValues: FormValues = {
};

export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClose, params }) => {
const store: RootStore = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;

Expand Down Expand Up @@ -71,16 +75,16 @@ export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClos
.then(() => {
setToastAlert({
type: "success",
title: "Success!",
message: "Analytics saved successfully.",
title: store.locale.localized("Success!"),
message: store.locale.localized("Analytics saved successfully."),
});
onClose();
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Analytics could not be saved. Please try again.",
title: store.locale.localized("Error!"),
message: store.locale.localized("Analytics could not be saved. Please try again."),
})
);
};
Expand Down Expand Up @@ -118,36 +122,40 @@ export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClos
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Save Analytics
{store.locale.localized("Save Analytics")}
</Dialog.Title>
<div className="mt-5">
<Input
type="text"
id="name"
name="name"
placeholder="Title"
placeholder={store.locale.localized("Title")}
autoComplete="off"
error={errors.name}
register={register}
width="full"
validations={{
required: "Title is required",
required: store.locale.localized("Title is required"),
}}
/>
<TextArea
id="description"
name="description"
placeholder="Description"
placeholder={store.locale.localized("Description")}
className="mt-3 h-32 resize-none text-sm"
error={errors.description}
register={register}
/>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<SecondaryButton onClick={onClose}>
{store.locale.localized("Cancel")}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Analytics"}
{isSubmitting
? store.locale.localized("Saving...")
: store.locale.localized("Save Analytics")}
</PrimaryButton>
</div>
</form>
Expand Down
16 changes: 13 additions & 3 deletions web/components/analytics/custom-analytics/custom-analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import { convertResponseToBarGraphData } from "helpers/analytics.helper";
import { IAnalyticsParams, IAnalyticsResponse, ICurrentUserResponse } from "types";
// fetch-keys
import { ANALYTICS } from "constants/fetch-keys";
// mobx
import { RootStore } from "store/root";
import { useMobxStore } from "lib/mobx/store-provider";

type Props = {
analytics: IAnalyticsResponse | undefined;
Expand All @@ -41,6 +44,7 @@ export const CustomAnalytics: React.FC<Props> = ({
fullScreen,
user,
}) => {
const store: RootStore = useMobxStore();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

Expand Down Expand Up @@ -87,7 +91,11 @@ export const CustomAnalytics: React.FC<Props> = ({
) : (
<div className="grid h-full place-items-center p-5">
<div className="space-y-4 text-custom-text-200">
<p className="text-sm">No matching issues found. Try changing the parameters.</p>
<p className="text-sm">
{store.locale.localized(
"No matching issues found. Try changing the parameters."
)}
</p>
</div>
</div>
)
Expand All @@ -105,7 +113,9 @@ export const CustomAnalytics: React.FC<Props> = ({
) : (
<div className="grid h-full place-items-center p-5">
<div className="space-y-4 text-custom-text-200">
<p className="text-sm">There was some error in fetching the data.</p>
<p className="text-sm">
{store.locale.localized("There was some error in fetching the data.")}
</p>
<div className="flex items-center justify-center gap-2">
<PrimaryButton
onClick={() => {
Expand All @@ -114,7 +124,7 @@ export const CustomAnalytics: React.FC<Props> = ({
mutate(ANALYTICS(workspaceSlug.toString(), params));
}}
>
Refresh
{store.locale.localized("Refresh")}
</PrimaryButton>
</div>
</div>
Expand Down
17 changes: 13 additions & 4 deletions web/components/analytics/custom-analytics/graph/custom-tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// nivo
import { BarTooltipProps } from "@nivo/bar";
import { DATE_KEYS } from "constants/analytics";
import { PRIORITIES_LABEL } from "constants/project";
import { STATE_GROUP_LABEL } from "constants/state";
import { renderMonthAndYear } from "helpers/analytics.helper";
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// types
import { IAnalyticsParams, IAnalyticsResponse } from "types";

Expand All @@ -12,22 +16,27 @@ type Props = {
};

export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) => {
const store: RootStore = useMobxStore();
let tooltipValue: string | number = "";

const renderAssigneeName = (assigneeId: string): string => {
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);

if (!assignee) return "No assignee";
if (!assignee) return store.locale.localized("No assignee");

return assignee.assignees__display_name || "No assignee";
return assignee.assignees__display_name || store.locale.localized("No assignee");
};

if (params.segment) {
if (DATE_KEYS.includes(params.segment)) tooltipValue = renderMonthAndYear(datum.id);
else tooltipValue = datum.id;
else tooltipValue = STATE_GROUP_LABEL[datum.id] || (PRIORITIES_LABEL[datum.id] ?? datum.id);
} else {
if (DATE_KEYS.includes(params.x_axis)) tooltipValue = datum.indexValue;
else tooltipValue = datum.id === "count" ? "Issue count" : "Estimate";
else
tooltipValue =
datum.id === "count"
? store.locale.localized("Issue count")
: store.locale.localized("Estimate");
}

return (
Expand Down
14 changes: 10 additions & 4 deletions web/components/analytics/custom-analytics/graph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { findStringWithMostCharacters } from "helpers/array.helper";
import { generateBarColor } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse } from "types";
import { STATE_GROUP_LABEL } from "constants/state";
import { PRIORITIES_LABEL } from "constants/project";

type Props = {
analytics: IAnalyticsResponse;
Expand Down Expand Up @@ -57,16 +59,20 @@ export const AnalyticsGraph: React.FC<Props> = ({
return data;
};

barGraphData.data.map((d) => {
d.label = STATE_GROUP_LABEL[d.name] ?? PRIORITIES_LABEL[d.name] ?? d.name;
});

const longestXAxisLabel = findStringWithMostCharacters(barGraphData.data.map((d) => `${d.name}`));

return (
<BarGraph
data={barGraphData.data}
indexBy="name"
indexBy="label"
keys={barGraphData.xAxisKeys}
colors={(datum) =>
generateBarColor(
params.segment ? `${datum.id}` : `${datum.indexValue}`,
params.segment ? `${datum.id}` : `${datum.data.name}`,
analytics,
params,
params.segment ? "segment" : "x_axis"
Expand Down Expand Up @@ -109,10 +115,10 @@ export const AnalyticsGraph: React.FC<Props> = ({
<circle cy={18} r={8} fill="#374151" />
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
{params.x_axis === "assignees__id"
? datum.value && datum.value !== "None"
? datum.value && datum.value !== "none"
? renderAssigneeName(datum.value)[0].toUpperCase()
: "?"
: datum.value && datum.value !== "None"
: datum.value && datum.value !== "none"
? `${datum.value}`.toUpperCase()[0]
: "?"}
</text>
Expand Down
Loading
Loading