Bundles in Crowdin enable you to export your project strings or a subset of them into a resource file that can be utilized during the build process of your product or even delivered to the end consumer via Crowdin OTA. Crowdin natively supports several bundle exporters and also has many exporters available as apps in the Crowdin Store.
This bundle exporter allows you to easily create your own file builder. It requires you to provide a Serverless function URL such as a Vercel Edge function or AWS Lambda. The custom bundle exporter will accept a request from Crowdin and proxy it to your function, which will provide a compiled resource file that will be delivered as a translated file from Crowdin.
Once you install this app and configure your Serverless function URL, you will be able to create a Bundle in Crowdin and select this exporter.
Your serverless function should do the following:
strings
array from the request payload and compile the resource file in the desired format.Below, you can find a sample payload as well as a sample function that you can deploy to Vercel.
export default async function handler(req, res) {
const { body } = req;
const result = body.strings.map(s => {
return {
id: s.identifier,
text: s.translations[body.languageId].text
}
})
return res.send(JSON.stringify(result));
}
Follow this guide to have it deployed to your Vercel.
Check out our minimalistic build function on Github.
Remember that your function should always return a string containing a contents of your resulting file.
This function will return plain JSON file containing Crowdin Identifier as a key and translations to the selected language as a value. Sample output would be:
[
{
"id": "identifier_1",
"text": "翻訳のための文字列 1"
},
{
"id": "identifier_2",
"text": "翻訳用文字列 2"
}
.....
]
You are welcome to contact the Crowdin Support team when you begin developing your own bundle exporter. Crowdin has 24x7 technical support available and is happy to assist you.
Your resulting resource file may use its own convention to represent placeholders in text. If you use the Unified Placeholders feature of your Crowdin project, you can create a function to convert Crowdin tags to the desired ones. The function below demonstrates how to convert Crowdin unified placeholders to the convention used in Flutter's ARB resources. If you do not use Unified placeholders, you can still convert the placeholder format used in your source strings to the placeholder format you need in your resulting file.
function convertPlaceholders(string) {
if (body.project.normalizePlaceholder) {
//Crowdin' unified placeholders look like [%s]
return string.replaceAll(/\[%([^\]]+)\]/gm, `{$1}`);
} else {
return string;
}
}
Many internationalization libraries support pluralization. Even if your format does not support it (e.g. if you use your own tools for i18n or the technology does not support plurals), you can always use ICU, a Unicode standard that is implemented for most major tech stacks. If your Crowdin string has plurals, the payload would look like the following:
{
id: 1864390,
identifier: 'apples_count',
context: 'apples_count [one] [other]',
customData: '',
maxLength: null,
isHidden: false,
hasPlurals: true,
labels: [],
text: { one: '[%s] apple', other: '[%s] apples' },
translations: {
uk: {
text: {
one: '[%s] яблучко',
few: '[%s] яблука',
many: '[%s] яблук',
other: '[%s] яблука'
},
status: {
one: 'approved',
few: 'translated',
many: 'translated',
other: 'translated'
}
}
}
}
Ukrainian language has four plural forms, all of which will be present in the payload. You can convert this object to the ICU syntax using the following or similar function
function serializeICU(identifier, translations) {
const pluralFormsTranslations = Object.entries(translations).map(([key, value]) => ` ${key} {${value}}`).join('');
return `{${identifier}, plural, ${pluralFormsTranslations}}`;
}
And the resulting ICU string will be like following:
{key, plural, one {[%s] яблучко} few {[%s] яблука} many {[%s] яблук} other {[%s] яблука}}
{
languageId: 'ja',
req: {
jobType: 'build-file',
organization: {
id: 200000009,
domain: 'serhiy',
baseUrl: 'https://serhiy.crowdin.com',
apiBaseUrl: 'https://serhiy.api.crowdin.com'
},
project: {
id: 398,
identifier: '1a57678bc6171e5713595b83e8ef8b90',
name: 'Unified Placeholders and Bundles'
},
file: { id: 14854, name: 'strings.xml', path: '' },
customSrxContents: null,
sourceLanguage: {
id: 'en',
name: 'English',
editorCode: 'en',
twoLettersCode: 'en',
threeLettersCode: 'eng',
locale: 'en-US',
androidCode: 'en-rUS',
osxCode: 'en.lproj',
osxLocale: 'en',
pluralCategoryNames: [ 'one', 'other' ],
pluralRules: '(n != 1)'
},
targetLanguages: [
{
id: 'ja',
name: 'Japanese',
editorCode: 'ja',
twoLettersCode: 'ja',
threeLettersCode: 'jpn',
locale: 'ja-JP',
androidCode: 'ja-rJP',
osxCode: 'ja.lproj',
osxLocale: 'ja',
pluralCategoryNames: [ 'other' ],
pluralRules: '0'
}
],
strings: [
{
id: 1864380,
identifier: 'identifier_1',
context: 'identifier_1',
customData: '',
maxLength: null,
isHidden: false,
hasPlurals: false,
labels: [],
text: 'String for translation 1',
translations: { ja: { text: '翻訳のための文字列 1', status: 'translated' } }
}
],
context: {
jwtPayload: {
aud: 'WKcvYqBU59RGSRzjoMzn',
sub: '2',
domain: 'serhiy',
context: { project_id: 398, organization_id: 200000009, user_id: 2 },
iat: 1672494603.015501,
exp: 1672495563.015501
},
clientId: 'serhiy__398__2',
crowdinId: 'serhiy'
},
project: {
id: 398,
groupId: 2,
userId: 2,
sourceLanguageId: 'en',
targetLanguageIds: [ 'ja', 'uk' ],
name: 'Unified Placeholders and Bundles',
identifier: '1a57678bc6171e5713595b83e8ef8b90',
description: '',
logo: 'data:image/png;base64,iV ...',
background: null,
isExternal: false,
externalType: null,
workflowId: 201,
hasCrowdsourcing: false,
publicDownloads: true,
createdAt: '2022-12-28T08:24:56+00:00',
updatedAt: '2022-12-28T08:24:56+00:00',
lastActivity: '2022-12-31T07:11:12+00:00',
targetLanguages: [
{
id: 'ja',
name: 'Japanese',
editorCode: 'ja',
twoLettersCode: 'ja',
threeLettersCode: 'jpn',
locale: 'ja-JP',
androidCode: 'ja-rJP',
osxCode: 'ja.lproj',
osxLocale: 'ja',
pluralCategoryNames: [ 'other' ],
pluralRules: '0',
pluralExamples: [ '0-999; 1.2...' ],
textDirection: 'ltr',
dialectOf: null
}
],
translateDuplicates: 0,
tagsDetection: 0,
glossaryAccess: false,
isMtAllowed: true,
hiddenStringsProofreadersAccess: true,
autoSubstitution: true,
exportTranslatedOnly: false,
skipUntranslatedStrings: false,
skipUntranslatedFiles: false,
exportWithMinApprovalsCount: 0,
exportStringsThatPassedWorkflow: false,
autoTranslateDialects: false,
normalizePlaceholder: true,
saveMetaInfoInSource: true,
inContext: false,
inContextProcessHiddenStrings: true,
inContextPseudoLanguageId: null,
inContextPseudoLanguage: null,
isSuspended: false,
qaCheckIsActive: true,
qaCheckCategories: {
empty: true,
size: true,
tags: true,
spaces: true,
variables: true,
punctuation: true,
symbolRegister: true,
specialSymbols: true,
wrongTranslation: true,
spellcheck: true,
icu: true,
terms: false,
duplicate: true,
ftl: true
},
customQaCheckIds: [],
languageMapping: {},
delayedWorkflowStart: false,
notificationSettings: {
translatorNewStrings: false,
managerNewStrings: false,
managerLanguageCompleted: false
}
}
}
Crowdin is a platform that helps you manage and translate content into different languages. Integrate Crowdin with your repo, CMS, or other systems. Source content is always up to date for your translators, and translated content is returned automatically.
Learn MoreReleased on Dec 31, 2022
Updated on May 23, 2024
Published by Awesome Crowdin
Identifier:custom-export