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

feat: crowdin updating #1246

Merged
merged 34 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
97f9b5a
migration: update crowdin string ids
JoeKarow Apr 24, 2024
caaa064
added gh pat error instructions
JoeKarow Apr 24, 2024
8678210
crowdin api fns
JoeKarow Apr 24, 2024
427d210
sonarlint issues - api
JoeKarow Apr 24, 2024
5d579b8
Merge branch 'dev' into IN-920-update-crowdin
JoeKarow Apr 24, 2024
6458602
Merge branch 'dev' into IN-920-update-crowdin
kodiakhq[bot] Apr 24, 2024
0812242
Merge branch 'dev' into IN-920-update-crowdin
kodiakhq[bot] Apr 24, 2024
b505c20
Merge branch 'dev' into IN-920-update-crowdin
kodiakhq[bot] Apr 25, 2024
d4d9886
Merge branch 'dev' into IN-920-update-crowdin
kodiakhq[bot] Apr 25, 2024
ebfb34d
Merge branch 'dev' into IN-920-update-crowdin
kodiakhq[bot] Apr 26, 2024
3fe193c
add suggestedBy col to suggestion table
JoeKarow Apr 25, 2024
b3ed623
save userId that suggested org
JoeKarow Apr 25, 2024
2e4827f
update renovate conf
JoeKarow Apr 25, 2024
022c6ca
update dep/scripts
JoeKarow Apr 25, 2024
b0cac52
add active flag for translation
JoeKarow Apr 25, 2024
d4860f7
update i18n generation/export
JoeKarow Apr 25, 2024
e074d00
add deps
JoeKarow Apr 26, 2024
ab31c50
update types
JoeKarow Apr 26, 2024
c60708b
crowdin api fns
JoeKarow Apr 26, 2024
80b755a
crowdin project/environment separation
JoeKarow Apr 26, 2024
828522b
wip: crowdin api integration
JoeKarow Apr 26, 2024
c758616
Merge branch 'dev' into IN-920-update-crowdin
kodiakhq[bot] Apr 26, 2024
b5ae4a9
Merge branch 'dev' into IN-920-update-crowdin
kodiakhq[bot] Apr 29, 2024
29bc18b
Merge branch 'dev' into IN-920-update-crowdin
kodiakhq[bot] Apr 29, 2024
654bea8
dep
JoeKarow Apr 29, 2024
4e9e4d3
update project info for testing
JoeKarow Apr 29, 2024
24cd37c
upsert key fn
JoeKarow Apr 30, 2024
1a89f86
crowdin integration
JoeKarow Apr 30, 2024
8855610
fix sonarlint issue
JoeKarow Apr 30, 2024
40cad8d
remove unused stuff
JoeKarow Apr 30, 2024
0b0dec1
crowdin integration
JoeKarow Apr 30, 2024
9a1616c
update types
JoeKarow Apr 30, 2024
9785992
update client to look at both projects
JoeKarow Apr 30, 2024
28db77e
cleanup imports
JoeKarow Apr 30, 2024
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
4 changes: 2 additions & 2 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
"packageRules": [
{
"groupName": "patched packages",
"matchPackageNames": ["@crowdin/ota-client", "trpc-panel", "json-schema-to-zod"],
"matchDepNames": ["@crowdin/ota-client", "trpc-panel", "json-schema-to-zod"],
"matchUpdateTypes": ["major", "minor", "patch"]
},
{
"enabled": false,
"groupName": "Ignored Versions",
"matchCurrentVersion": "0.9.2",
"matchPackageNames": ["@t3-oss/env-nextjs"]
"matchDepNames": ["@t3-oss/env-nextjs"]
}
],
"semanticCommitScope": "{{parentDir}}"
Expand Down
9 changes: 6 additions & 3 deletions apps/app/lib/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,21 @@ import { generateTranslationKeys } from 'lib/generators'
const program = new Command()

export type PassedTask = ListrTaskWrapper<unknown, ListrDefaultRenderer, ListrSimpleRenderer>
type TaskDef = ListrTask<unknown, ListrDefaultRenderer, ListrSimpleRenderer>

const options = {
const rendererOptions: TaskDef['rendererOptions'] = {
bottomBar: 10,
persistentOutput: true,
outputBar: true,
}
const translation = [
{
title: 'Translation definitions from DB',
task: (_ctx: ListrContext, task: PassedTask) => generateTranslationKeys(task),
skip: !process.env.DATABASE_URL,
options,
rendererOptions,
},
]
] satisfies TaskDef[]

program
.name('generate')
Expand All @@ -47,6 +49,7 @@ if (Object.keys(cliOpts).length === 0) {

const tasks = new Listr(tasklist, {
exitOnError: false,
rendererOptions: { collapseSubtasks: false },
})

tasks.run()
Expand Down
86 changes: 47 additions & 39 deletions apps/app/lib/generators/translationKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,28 @@ const isObject = (data: unknown): data is Record<string, string> =>

const countKeys = (obj: Output): number => Object.keys(flatten(obj)).length

export const generateTranslationKeys = async (task: PassedTask) => {
const prettierOpts = (await prettier.resolveConfig(__filename)) ?? undefined

const where = (): Prisma.TranslationNamespaceWhereInput | undefined => {
switch (true) {
case !!process.env.EXPORT_ALL: {
return undefined
}
case !!process.env.EXPORT_DB: {
return { name: 'org-data' }
}
default: {
return { exportFile: true }
const where = (): Prisma.TranslationNamespaceWhereInput | undefined => {
switch (true) {
case !!process.env.EXPORT_ALL: {
return undefined
}
case !!process.env.EXPORT_DB: {
return {
name: 'org-data',
}
}
default: {
return { exportFile: true }
}
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
}
}

const data = await prisma.translationNamespace.findMany({
const getKeysFromDb = async () =>
await prisma.translationNamespace.findMany({
where: where(),
include: {
keys: {
...(!process.env.EXPORT_INACTIVE && { where: { active: true } }),
orderBy: {
key: 'asc',
},
Expand All @@ -51,52 +52,59 @@ export const generateTranslationKeys = async (task: PassedTask) => {
name: 'asc',
},
})

type DBKeys = Prisma.PromiseReturnType<typeof getKeysFromDb>[number]['keys']

const processKeys = (keys: DBKeys) => {
const outputData: Output = {}
for (const item of keys) {
if (item.interpolation && isObject(item.interpolationValues)) {
for (const [context, textContent] of Object.entries(item.interpolationValues)) {
if (typeof textContent !== 'string') {
throw new Error('Invalid nested plural item')
}
outputData[`${item.key}_${context}`] = textContent
}
}
if (!item.interpolation || item.interpolation === 'CONTEXT') {
outputData[item.key] = item.text
}
}
return outputData
}

export const generateTranslationKeys = async (task: PassedTask) => {
const prettierConfig = (await prettier.resolveConfig(__filename, { editorconfig: true })) ?? undefined
const prettierOpts = prettierConfig ? { ...prettierConfig, parser: 'json' } : undefined
const data = await getKeysFromDb()
let logMessage = ''
let i = 0
task.output = `Fetched ${data.length} namespaces from DB`
for (const namespace of data) {
const outputData: Output = {}
for (const item of namespace.keys) {
if (item.interpolation && isObject(item.interpolationValues)) {
for (const [key, value] of Object.entries(item.interpolationValues)) {
if (typeof value !== 'string') {
throw new Error('Invalid nested plural item')
}
outputData[`${item.key}_${key}`] = value
}
}
if (item.ns === 'attribute') {
outputData[item.key] = item.text
}
}
const outputData = processKeys(namespace.keys)
const filename = `${localePath}/${namespace.name}.json`

let existingFile: unknown = {}
if (fs.existsSync(filename)) {
if (fs.existsSync(filename) && !namespace.overwriteFileOnExport) {
existingFile = flatten(JSON.parse(fs.readFileSync(filename, 'utf-8')))
}
if (!isOutput(existingFile)) {
throw new Error("tried to load file, but it's empty")
}
// const existingLength = Object.keys(existingFile).length

const existingLength = countKeys(existingFile)
let outputFile: Output = unflatten(Object.assign(existingFile, outputData), { overwrite: true })
outputFile = Object.keys(outputFile)
.sort((a, b) => a.localeCompare(b))
.toSorted((a, b) => a.localeCompare(b))
.reduce((obj: Record<string, string>, key) => {
obj[key] = outputFile[key] as string
return obj
}, {})

const newKeys = countKeys(outputFile) - existingLength
logMessage = `${filename} generated with ${newKeys} new ${newKeys === 1 ? 'key' : 'keys'}.`

const formattedOutput = await prettier.format(JSON.stringify(outputFile), {
...prettierOpts,
parser: 'json',
})
fs.writeFileSync(filename, formattedOutput)

logMessage = `${filename} generated with ${newKeys} ${namespace.overwriteFileOnExport ? 'total' : 'new'} ${newKeys === 1 ? 'key' : 'keys'}.`
const formattedOutput = await prettier.format(JSON.stringify(outputFile), prettierOpts)
fs.writeFileSync(filename, formattedOutput, 'utf-8')
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
task.output = logMessage
i++
}
Expand Down
4 changes: 2 additions & 2 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"postdev": "pnpm -w docker:down",
"dev:verbose": "NEXT_VERBOSE=1 next dev",
"format": "prettier --write --ignore-unknown .",
"generate:all": "tsx ./lib/generate.ts",
"generate:i18n": "tsx ./lib/generate.ts -t",
"generate:all": "pnpm with-env tsx ./lib/generate.ts",
"generate:i18n": "pnpm with-env tsx ./lib/generate.ts -t",
"generate:i18nTypes": "i18next-resources-for-ts interface -i ./public/locales/en -o ./src/types/resources.d.ts",
"preinstall": "npx only-allow pnpm",
"lint": "next lint",
Expand Down
20 changes: 10 additions & 10 deletions apps/app/src/pages/api/i18n/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export const config = {
}

const QuerySchema = z.object({
lng: z.string(),
ns: z.string(),
lng: z.string().transform((s) => s.split(' ')),
ns: z.string().transform((s) => s.split(' ')),
})
const tracer = trace.getTracer('inreach-app')
const log = createLoggerInstance('i18n Loader')
Expand All @@ -34,10 +34,9 @@ export default async function handler(req: NextRequest) {
})
}
const query = parsedQuery.data
const namespaces = query.ns.split(' ')
const langs = query.lng.split(' ')
const { ns: namespaces, lng: langs } = query
const cacheWriteQueue: WriteCacheArgs[] = []
const otaManifestTimestamp = await crowdinDistTimestamp()
const otaManifestTimestamps = await crowdinDistTimestamp()
const results = new Map<string, object>()

for (const lang of langs) {
Expand All @@ -46,19 +45,20 @@ export default async function handler(req: NextRequest) {
if (lang === 'en') {
continue
}
const databaseFile = sourceFiles(lang).databaseStrings
const cached = await redisReadCache(namespaces, lang, otaManifestTimestamp)
const cached = await redisReadCache(namespaces, lang, otaManifestTimestamps)
const langResult = new Map<string, object | string>(cached)

const fetchCrowdin = async (ns: string) => {
const crowdinSpan = tracer.startSpan('Crowdin OTA', undefined, context.active())
try {
crowdinSpan.setAttributes({ ns })
switch (true) {
// Check if the namespace is already in the cache
case langResult.has(ns): {
return
}
case Object.hasOwn(nsFileMap, ns): {
// Check if the namespace is file based
case ns in nsFileMap: {
const file = nsFileMap[ns as keyof typeof nsFileMap] ?? ''
const strings = await fetchCrowdinFile(file, lang)
if (strings && Object.keys(strings).length) {
Expand All @@ -67,9 +67,9 @@ export default async function handler(req: NextRequest) {
langResult.set(ns, strings)
break
}
// Otherwise, it must be a database key
default: {
const file = databaseFile
const strings = await fetchCrowdinDbKey(ns, file, lang)
const strings = await fetchCrowdinDbKey(ns, lang)
if (strings) {
cacheWriteQueue.push({ lang, ns, strings })
}
Expand Down
21 changes: 17 additions & 4 deletions packages/api/router/orgEmail/mutation.create.handler.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { addSingleKey } from '@weareinreach/crowdin/api'
import { getAuditedClient } from '@weareinreach/db'
import { type TRPCHandlerParams } from '~api/types/handler'

import { type TCreateSchema } from './mutation.create.schema'

const create = async ({ ctx, input }: TRPCHandlerParams<TCreateSchema, 'protected'>) => {
const prisma = getAuditedClient(ctx.actorId)
const newEmail = await prisma.orgEmail.create({
data: input,
select: { id: true },

const result = await prisma.$transaction(async (tx) => {
if (input.description) {
const crowdinId = await addSingleKey({
isDatabaseString: true,
key: input.description.create.tsKey.create.key,
text: input.description.create.tsKey.create.text,
})
input.description.create.tsKey.create.crowdinId = crowdinId.id
}
const newEmail = await tx.orgEmail.create({
data: input,
select: { id: true },
})
return newEmail
})
return newEmail
return result
}
export default create
38 changes: 22 additions & 16 deletions packages/api/router/orgEmail/mutation.create.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,33 @@ export const ZCreateSchema = z
.transform(({ orgId, data, title, titleId, description }) => {
const id = generateId('orgEmail')

const handleTitle = () => {
if (title) {
return {
create: {
title,
key: {
create: {
text: title,
key: slug(title),
namespace: { connect: { name: namespace.userTitle } },
},
},
},
}
}
if (titleId) {
return { connect: { id: titleId } }
}
return undefined
}

return Prisma.validator<Prisma.OrgEmailCreateInput>()({
...data,
description: description
? generateNestedFreeText({ orgId, itemId: id, text: description, type: 'emailDesc' })
: undefined,
title: title
? {
create: {
title,
key: {
create: {
text: title,
key: slug(title),
namespace: { connect: { name: namespace.userTitle } },
},
},
},
}
: titleId
? { connect: { id: titleId } }
: undefined,
title: handleTitle(),
})
})
export type TCreateSchema = z.infer<typeof ZCreateSchema>
Loading
Loading